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 = $item->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/Circulation.pm (+6 lines)
Lines 2433-2438 sub AddReturn { Link Here
2433
        }
2433
        }
2434
    }
2434
    }
2435
2435
2436
    # Check for bundle status
2437
    if ( $item->in_bundle ) {
2438
        my $host = $item->bundle_host;
2439
        $messages->{InBundle} = $host;
2440
    }
2441
2436
    my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2442
    my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX });
2437
    $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2443
    $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" );
2438
2444
(-)a/Koha/Checkouts/ReturnClaims.pm (-1 / +1 lines)
Lines 60-66 sub resolved { Link Here
60
    return Koha::Checkouts::ReturnClaims->_new_from_dbic( $results );
60
    return Koha::Checkouts::ReturnClaims->_new_from_dbic( $results );
61
}
61
}
62
62
63
=head3 type
63
=head3 _type
64
64
65
=cut
65
=cut
66
66
(-)a/Koha/Item.pm (-1 / +190 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 440-445 sub item_group { Link Here
440
    return $item_group;
441
    return $item_group;
441
}
442
}
442
443
444
=head3 return_claims
445
446
  my $return_claims = $item->return_claims;
447
448
Return any return_claims associated with this item
449
450
=cut
451
452
sub return_claims {
453
    my ( $self, $params, $attrs ) = @_;
454
    my $claims_rs = $self->_result->return_claims->search($params, $attrs);
455
    return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
456
}
457
458
=head3 return_claim
459
460
  my $return_claim = $item->return_claim;
461
462
Returns the most recent unresolved return_claims associated with this item
463
464
=cut
465
466
sub return_claim {
467
    my ($self) = @_;
468
    my $claims_rs =
469
      $self->_result->return_claims->search( { resolution => undef },
470
        { order_by => { '-desc' => 'created_on' }, rows => 1 } )->single;
471
    return unless $claims_rs;
472
    return Koha::Checkouts::ReturnClaim->_new_from_dbic($claims_rs);
473
}
474
443
=head3 holds
475
=head3 holds
444
476
445
my $holds = $item->holds();
477
my $holds = $item->holds();
Lines 1113-1119 Internal function, not exported, called only by Koha::Item->store. Link Here
1113
sub _set_found_trigger {
1145
sub _set_found_trigger {
1114
    my ( $self, $pre_mod_item ) = @_;
1146
    my ( $self, $pre_mod_item ) = @_;
1115
1147
1116
    ## If item was lost, it has now been found, reverse any list item charges if necessary.
1148
    # Reverse any lost item charges if necessary.
1117
    my $no_refund_after_days =
1149
    my $no_refund_after_days =
1118
      C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1150
      C4::Context->preference('NoRefundOnLostReturnedItemsAge');
1119
    if ($no_refund_after_days) {
1151
    if ($no_refund_after_days) {
Lines 1465-1470 sub move_to_biblio { Link Here
1465
    return $to_biblionumber;
1497
    return $to_biblionumber;
1466
}
1498
}
1467
1499
1500
=head3 bundle_items
1501
1502
  my $bundle_items = $item->bundle_items;
1503
1504
Returns the items associated with this bundle
1505
1506
=cut
1507
1508
sub bundle_items {
1509
    my ($self) = @_;
1510
1511
    if ( !$self->{_bundle_items_cached} ) {
1512
        my $bundle_items = Koha::Items->search(
1513
            { 'item_bundles_item.host' => $self->itemnumber },
1514
            { join                     => 'item_bundles_item' } );
1515
        $self->{_bundle_items}        = $bundle_items;
1516
        $self->{_bundle_items_cached} = 1;
1517
    }
1518
1519
    return $self->{_bundle_items};
1520
}
1521
1522
=head3 is_bundle
1523
1524
  my $is_bundle = $item->is_bundle;
1525
1526
Returns whether the item is a bundle or not
1527
1528
=cut
1529
1530
sub is_bundle {
1531
    my ($self) = @_;
1532
    return $self->bundle_items->count ? 1 : 0;
1533
}
1534
1535
=head3 bundle_host
1536
1537
  my $bundle = $item->bundle_host;
1538
1539
Returns the bundle item this item is attached to
1540
1541
=cut
1542
1543
sub bundle_host {
1544
    my ($self) = @_;
1545
1546
    my $bundle_items_rs = $self->_result->item_bundles_item;
1547
    return unless $bundle_items_rs;
1548
    return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1549
}
1550
1551
=head3 in_bundle
1552
1553
  my $in_bundle = $item->in_bundle;
1554
1555
Returns whether this item is currently in a bundle
1556
1557
=cut
1558
1559
sub in_bundle {
1560
    my ($self) = @_;
1561
    return $self->bundle_host ? 1 : 0;
1562
}
1563
1564
=head3 add_to_bundle
1565
1566
  my $link = $item->add_to_bundle($bundle_item);
1567
1568
Adds the bundle_item passed to this item
1569
1570
=cut
1571
1572
sub add_to_bundle {
1573
    my ( $self, $bundle_item ) = @_;
1574
1575
    my $schema = Koha::Database->new->schema;
1576
1577
    my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1578
1579
    try {
1580
        $schema->txn_do(
1581
            sub {
1582
                $self->_result->add_to_item_bundles_hosts(
1583
                    { item => $bundle_item->itemnumber } );
1584
1585
                $bundle_item->notforloan($BundleNotLoanValue)->store();
1586
            }
1587
        );
1588
    }
1589
    catch {
1590
1591
        # 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 from DBIx::Error
1592
        if ( ref($_) eq 'DBIx::Class::Exception' ) {
1593
            warn $_->{msg};
1594
            if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1595
                # FK constraints
1596
                # FIXME: MySQL error, if we support more DB engines we should implement this for each
1597
                if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1598
                    Koha::Exceptions::Object::FKConstraint->throw(
1599
                        error     => 'Broken FK constraint',
1600
                        broken_fk => $+{column}
1601
                    );
1602
                }
1603
            }
1604
            elsif (
1605
                $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1606
            {
1607
                Koha::Exceptions::Object::DuplicateID->throw(
1608
                    error        => 'Duplicate ID',
1609
                    duplicate_id => $+{key}
1610
                );
1611
            }
1612
            elsif ( $_->{msg} =~
1613
/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1614
              )
1615
            {    # The optional \W in the regex might be a quote or backtick
1616
                my $type     = $+{type};
1617
                my $value    = $+{value};
1618
                my $property = $+{property};
1619
                $property =~ s/['`]//g;
1620
                Koha::Exceptions::Object::BadValue->throw(
1621
                    type     => $type,
1622
                    value    => $value,
1623
                    property => $property =~ /(\w+\.\w+)$/
1624
                    ? $1
1625
                    : $property
1626
                    ,    # results in table.column without quotes or backtics
1627
                );
1628
            }
1629
1630
            # Catch-all for foreign key breakages. It will help find other use cases
1631
            $_->rethrow();
1632
        }
1633
        else {
1634
            $_;
1635
        }
1636
    };
1637
}
1638
1639
=head3 remove_from_bundle
1640
1641
Remove this item from any bundle it may have been attached to.
1642
1643
=cut
1644
1645
sub remove_from_bundle {
1646
    my ($self) = @_;
1647
1648
    my $bundle_item_rs = $self->_result->item_bundles_item;
1649
    if ( $bundle_item_rs ) {
1650
        $bundle_item_rs->delete;
1651
        $self->notforloan(0)->store();
1652
        return 1;
1653
    }
1654
    return 0;
1655
}
1656
1468
=head2 Internal methods
1657
=head2 Internal methods
1469
1658
1470
=head3 _after_item_action_hooks
1659
=head3 _after_item_action_hooks
(-)a/Koha/REST/V1/Items.pm (+124 lines)
Lines 19-24 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious::Controller';
20
use Mojo::Base 'Mojolicious::Controller';
21
21
22
use C4::Circulation qw( barcodedecode );
23
22
use Koha::Items;
24
use Koha::Items;
23
25
24
use List::MoreUtils qw( any );
26
use List::MoreUtils qw( any );
Lines 148-151 sub pickup_locations { Link Here
148
    };
150
    };
149
}
151
}
150
152
153
=head3 bundled_items
154
155
Controller function that handles bundled_items Koha::Item objects
156
157
=cut
158
159
sub bundled_items {
160
    my $c = shift->openapi->valid_input or return;
161
162
    my $item_id = $c->validation->param('item_id');
163
    my $item = Koha::Items->find( $item_id );
164
165
    unless ($item) {
166
        return $c->render(
167
            status  => 404,
168
            openapi => { error => "Item not found" }
169
        );
170
    }
171
172
    return try {
173
        my $items_set = $item->bundle_items;
174
        my $items     = $c->objects->search( $items_set );
175
        return $c->render(
176
            status  => 200,
177
            openapi => $items
178
        );
179
    }
180
    catch {
181
        $c->unhandled_exception($_);
182
    };
183
}
184
185
=head3 add_to_bundle
186
187
Controller function that handles adding items to this bundle
188
189
=cut
190
191
sub add_to_bundle {
192
    my $c = shift->openapi->valid_input or return;
193
194
    my $item_id = $c->validation->param('item_id');
195
    my $item = Koha::Items->find( $item_id );
196
197
    unless ($item) {
198
        return $c->render(
199
            status  => 404,
200
            openapi => { error => "Item not found" }
201
        );
202
    }
203
204
    my $bundle_item_id = $c->validation->param('body')->{'external_id'};
205
    $bundle_item_id = barcodedecode($bundle_item_id);
206
    my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } );
207
208
    unless ($bundle_item) {
209
        return $c->render(
210
            status  => 404,
211
            openapi => { error => "Bundle item not found" }
212
        );
213
    }
214
215
    return try {
216
        my $link = $item->add_to_bundle($bundle_item);
217
        return $c->render(
218
            status  => 201,
219
            openapi => $bundle_item
220
        );
221
    }
222
    catch {
223
        if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) {
224
            return $c->render(
225
                status  => 409,
226
                openapi => {
227
                    error => 'Item is already bundled',
228
                    key   => $_->duplicate_id
229
                }
230
            );
231
        }
232
        else {
233
            $c->unhandled_exception($_);
234
        }
235
    };
236
}
237
238
=head3 remove_from_bundle
239
240
Controller function that handles removing items from this bundle
241
242
=cut
243
244
sub remove_from_bundle {
245
    my $c = shift->openapi->valid_input or return;
246
247
    my $item_id = $c->validation->param('item_id');
248
    my $item = Koha::Items->find( $item_id );
249
250
    unless ($item) {
251
        return $c->render(
252
            status  => 404,
253
            openapi => { error => "Item not found" }
254
        );
255
    }
256
257
    my $bundle_item_id = $c->validation->param('bundled_item_id');
258
    $bundle_item_id = barcodedecode($bundle_item_id);
259
    my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } );
260
261
    unless ($bundle_item) {
262
        return $c->render(
263
            status  => 404,
264
            openapi => { error => "Bundle item not found" }
265
        );
266
    }
267
268
    $bundle_item->remove_from_bundle;
269
    return $c->render(
270
        status  => 204,
271
        openapi => q{}
272
    );
273
}
274
151
1;
275
1;
(-)a/Koha/Schema/Result/Item.pm (+30 lines)
Lines 744-749 __PACKAGE__->might_have( Link Here
744
  { cascade_copy => 0, cascade_delete => 0 },
744
  { cascade_copy => 0, cascade_delete => 0 },
745
);
745
);
746
746
747
=head2 item_bundles_hosts
748
749
Type: has_many
750
751
Related object: L<Koha::Schema::Result::ItemBundle>
752
753
=cut
754
755
__PACKAGE__->has_many(
756
  "item_bundles_hosts",
757
  "Koha::Schema::Result::ItemBundle",
758
  { "foreign.host" => "self.itemnumber" },
759
  { cascade_copy => 0, cascade_delete => 0 },
760
);
761
762
=head2 item_bundles_item
763
764
Type: might_have
765
766
Related object: L<Koha::Schema::Result::ItemBundle>
767
768
=cut
769
770
__PACKAGE__->might_have(
771
  "item_bundles_item",
772
  "Koha::Schema::Result::ItemBundle",
773
  { "foreign.item" => "self.itemnumber" },
774
  { cascade_copy => 0, cascade_delete => 0 },
775
);
776
747
=head2 items_last_borrower
777
=head2 items_last_borrower
748
778
749
Type: might_have
779
Type: might_have
(-)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 @ 2022-03-31 13:56:51
109
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:T02Qk/Ojl7OXlL62uQk6Og
110
111
112
# You can replace this text with custom code or comments, and it will be preserved on regeneration
113
1;
(-)a/Koha/Schema/Result/ReturnClaim.pm (-16 / +2 lines)
Lines 170-189 __PACKAGE__->add_columns( Link Here
170
170
171
__PACKAGE__->set_primary_key("id");
171
__PACKAGE__->set_primary_key("id");
172
172
173
=head1 UNIQUE CONSTRAINTS
174
175
=head2 C<issue_id>
176
177
=over 4
178
179
=item * L</issue_id>
180
181
=back
182
183
=cut
184
185
__PACKAGE__->add_unique_constraint("issue_id", ["issue_id"]);
186
187
=head1 RELATIONS
173
=head1 RELATIONS
188
174
189
=head2 borrowernumber
175
=head2 borrowernumber
Lines 277-284 __PACKAGE__->belongs_to( Link Here
277
);
263
);
278
264
279
265
280
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-17 10:01:24
266
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-11-18 15:07:03
281
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Ik93SD3kLNecIyRgsBVKDQ
267
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HtOvm4y611GNQcPwKzY1jg
282
268
283
=head2 checkout
269
=head2 checkout
284
270
(-)a/admin/columns_settings.yml (+18 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: callnumber
537
            -
538
              columnname: barcode
539
            -
540
              columnname: status
541
            -
542
              columnname: bundle_actions
543
              cannot_be_toggled: 1
544
              cannot_be_modified: 1
545
528
  cataloguing:
546
  cataloguing:
529
    addbooks:
547
    addbooks:
530
      reservoir-table:
548
      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 (+9 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
Lines 211-216 properties: Link Here
211
  exclude_from_local_holds_priority:
212
  exclude_from_local_holds_priority:
212
    type: boolean
213
    type: boolean
213
    description: Exclude this item from local holds priority.
214
    description: Exclude this item from local holds priority.
215
  return_claims:
216
    type: array
217
    description: An array of all return claims associated with this item
218
  return_claim:
219
    type:
220
      - object
221
      - "null"
222
    description: A return claims object if one exists that's unresolved
214
additionalProperties: false
223
additionalProperties: false
215
required:
224
required:
216
  - item_id
225
  - item_id
(-)a/api/v1/swagger/paths/items.yaml (+160 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
      - return_claims
206
      - return_claim
207
      - return_claim.patron
208
"/items/{item_id}/bundled_items/{bundled_item_id}":
209
  delete:
210
    x-mojo-to: Items#remove_from_bundle
211
    operationId: removeFromBundle
212
    tags:
213
      - items
214
    summary: Remove item from bundle
215
    parameters:
216
      - $ref: "../swagger.yaml#/parameters/item_id_pp"
217
      - name: bundled_item_id
218
        in: path
219
        description: Internal identifier for the bundled item
220
        required: true
221
        type: string
222
    consumes:
223
      - application/json
224
    produces:
225
      - application/json
226
    responses:
227
      "204":
228
        description: Bundle link deleted
229
      "400":
230
        description: Bad parameter
231
        schema:
232
          $ref: "../swagger.yaml#/definitions/error"
233
      "401":
234
        description: Authentication required
235
        schema:
236
          $ref: "../swagger.yaml#/definitions/error"
237
      "403":
238
        description: Access forbidden
239
        schema:
240
          $ref: "../swagger.yaml#/definitions/error"
241
      "404":
242
        description: Resource not found
243
        schema:
244
          $ref: "../swagger.yaml#/definitions/error"
245
      "500":
246
        description: Internal server error
247
        schema:
248
          $ref: "../swagger.yaml#/definitions/error"
249
      "503":
250
        description: Under maintenance
251
        schema:
252
          $ref: "../swagger.yaml#/definitions/error"
253
    x-koha-authorization:
254
      permissions:
255
        catalogue: 1
96
"/items/{item_id}/pickup_locations":
256
"/items/{item_id}/pickup_locations":
97
  get:
257
  get:
98
    x-mojo-to: Items#pickup_locations
258
    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 (+18 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
        $item->{bundled} =
461
          $item_object->bundle_items->search( { itemlost => { '!=' => 0 } } )
462
          ->count;
463
        $item->{bundled_lost} =
464
          $item_object->bundle_items->search( { itemlost => 0 } )->count;
465
        $item->{is_bundle} = 1;
466
    }
467
468
    if ($item_object->in_bundle) {
469
        $item->{bundle_host} = $item_object->bundle_host;
470
    }
471
454
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
472
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
455
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
473
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
456
            push @itemloop, $item;
474
            push @itemloop, $item;
(-)a/circ/returns.pl (-2 / +96 lines)
Lines 338-347 if ($barcode) { Link Here
338
    $template->param( 'multiple_confirmed' => 1 )
338
    $template->param( 'multiple_confirmed' => 1 )
339
      if $query->param('multiple_confirm');
339
      if $query->param('multiple_confirm');
340
340
341
    # Block return if bundle and confirm has not been received
342
    my $bundle_confirm =
343
         $item
344
      && $item->is_bundle
345
      && !$query->param('confirm_items_bundle_return');
346
    $template->param( 'confirm_items_bundle_returned' => 1 )
347
      if $query->param('confirm_items_bundle_return');
348
341
    # do the return
349
    # do the return
342
    ( $returned, $messages, $issue, $borrower ) =
350
    ( $returned, $messages, $issue, $borrower ) =
343
      AddReturn( $barcode, $userenv_branch, $exemptfine, $return_date )
351
      AddReturn( $barcode, $userenv_branch, $exemptfine, $return_date )
344
          unless $needs_confirm;
352
          unless ( $needs_confirm || $bundle_confirm );
345
353
346
    if ($returned) {
354
    if ($returned) {
347
        my $time_now = dt_from_string()->truncate( to => 'minute');
355
        my $time_now = dt_from_string()->truncate( to => 'minute');
Lines 382-388 if ($barcode) { Link Here
382
                );
390
                );
383
            }
391
            }
384
        }
392
        }
385
    } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm ) {
393
394
    } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm and !$bundle_confirm ) {
386
        $input{duedate}   = 0;
395
        $input{duedate}   = 0;
387
        $returneditems{0} = $barcode;
396
        $returneditems{0} = $barcode;
388
        $riduedate{0}     = 0;
397
        $riduedate{0}     = 0;
Lines 393-398 if ($barcode) { Link Here
393
    if ( $needs_confirm ) {
402
    if ( $needs_confirm ) {
394
        $template->param( needs_confirm => $needs_confirm );
403
        $template->param( needs_confirm => $needs_confirm );
395
    }
404
    }
405
406
    if ( $bundle_confirm ) {
407
        $template->param(
408
            items_bundle_return_confirmation => 1,
409
        );
410
    }
411
412
    # Mark missing bundle items as lost and report unexpected items
413
    if ( $item->is_bundle && $query->param('confirm_items_bundle_return') ) {
414
        my $BundleLostValue = C4::Context->preference('BundleLostValue');
415
        my $barcodes = $query->param('verify-items-bundle-contents-barcodes');
416
        my @barcodes = map { s/^\s+|\s+$//gr } ( split /\n/, $barcodes );
417
        my $expected_items = { map { $_->barcode => $_ } $item->bundle_items->as_list };
418
        my $verify_items = Koha::Items->search( { barcode => { 'in' => \@barcodes } } );
419
        my @unexpected_items;
420
        my @missing_items;
421
        my @bundle_items;
422
        while ( my $verify_item = $verify_items->next ) {
423
            # Fix and lost statuses
424
            $verify_item->itemlost(0);
425
426
            # Update last_seen
427
            $verify_item->datelastseen( dt_from_string()->ymd() );
428
429
            # Update last_borrowed if actual checkin
430
            $verify_item->datelastborrowed( dt_from_string()->ymd() ) if $issue;
431
432
            # Expected item, remove from lookup table
433
            if ( delete $expected_items->{$verify_item->barcode} ) {
434
                push @bundle_items, $verify_item;
435
            }
436
            # Unexpected item, warn and remove from bundle
437
            else {
438
                $verify_item->remove_from_bundle;
439
                push @unexpected_items, $verify_item;
440
            }
441
442
            # Store results
443
            $verify_item->store();
444
        }
445
        for my $missing_item ( keys %{$expected_items} ) {
446
            my $bundle_item = $expected_items->{$missing_item};
447
            # Mark as lost if it's not already lost
448
            if ( !$bundle_item->itemlost ) {
449
                $bundle_item->itemlost($BundleLostValue)->store();
450
451
                # Add return_claim record if this is an actual checkin
452
                if ($issue) {
453
                    $bundle_item->_result->create_related(
454
                        'return_claims',
455
                        {
456
                            issue_id       => $issue->issue_id,
457
                            itemnumber     => $bundle_item->itemnumber,
458
                            borrowernumber => $issue->borrowernumber,
459
                            created_by     => C4::Context->userenv()->{number},
460
                            created_on     => dt_from_string
461
                        }
462
                    );
463
                }
464
                push @missing_items, $bundle_item;
465
466
                # NOTE: We cannot use C4::LostItem here because the item itself doesn't have a checkout
467
                # and thus would not get charged.. it's checked out as part of the bundle.
468
                if ( C4::Context->preference('WhenLostChargeReplacementFee') && $issue ) {
469
                    C4::Accounts::chargelostitem(
470
                        $issue->borrowernumber,
471
                        $bundle_item->itemnumber,
472
                        $bundle_item->replacementprice,
473
                        sprintf( "%s %s %s",
474
                            $bundle_item->biblio->title  || q{},
475
                            $bundle_item->barcode        || q{},
476
                            $bundle_item->itemcallnumber || q{},
477
                        ),
478
                    );
479
                }
480
            }
481
        }
482
        $template->param(
483
            unexpected_items => \@unexpected_items,
484
            missing_items    => \@missing_items,
485
            bundle_items     => \@bundle_items
486
        );
487
    }
396
}
488
}
397
$template->param( inputloop => \@inputloop );
489
$template->param( inputloop => \@inputloop );
398
490
Lines 631-636 foreach my $code ( keys %$messages ) { Link Here
631
        ;
723
        ;
632
    } elsif ( $code eq 'TransferredRecall' ) {
724
    } elsif ( $code eq 'TransferredRecall' ) {
633
        ;
725
        ;
726
    } elsif ( $code eq 'InBundle' ) {
727
        $template->param( InBundle => $messages->{InBundle} );
634
    } else {
728
    } else {
635
        die "Unknown error code $code";    # note we need all the (empty) elsif's above, or we die.
729
        die "Unknown error code $code";    # note we need all the (empty) elsif's above, or we die.
636
        # This forces the issue of staying in sync w/ Circulation.pm
730
        # This forces the issue of staying in sync w/ Circulation.pm
(-)a/installer/data/mysql/atomicupdate/bug_28854.pl (+55 lines)
Line 0 Link Here
1
use Modern::Perl;
2
3
return {
4
    bug_number => "28854",
5
    description => "Item bundles support",
6
    up => sub {
7
        my ($args) = @_;
8
        my ($dbh, $out) = @$args{qw(dbh out)};
9
10
        if( !TableExists( 'item_bundles' ) ) {
11
            $dbh->do(q{
12
                CREATE TABLE `item_bundles` (
13
                  `item` int(11) NOT NULL,
14
                  `host` int(11) NOT NULL,
15
                  PRIMARY KEY (`host`, `item`),
16
                  UNIQUE KEY `item_bundles_uniq_1` (`item`),
17
                  CONSTRAINT `item_bundles_ibfk_1` FOREIGN KEY (`item`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
18
                  CONSTRAINT `item_bundles_ibfk_2` FOREIGN KEY (`host`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE
19
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
20
            });
21
        }
22
        say $out "item_bundles table added";
23
24
        my ($lost_val) = $dbh->selectrow_array( "SELECT MAX(authorised_value) FROM authorised_values WHERE category = 'LOST'", {} );
25
        $lost_val++;
26
27
        $dbh->do(qq{
28
           INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('LOST',$lost_val,'Missing from bundle')
29
        });
30
        say $out "Missing from bundle LOST AV added";
31
32
        my ($nfl_val) = $dbh->selectrow_array( "SELECT MAX(authorised_value) FROM authorised_values WHERE category = 'NOT_LOAN'", {} );
33
        $nfl_val++;
34
35
        $dbh->do(qq{
36
           INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('NOT_LOAN',$nfl_val,'Added to bundle')
37
        });
38
        say $out "Added to bundle NOT_LOAN AV added";
39
40
        $dbh->do(qq{
41
            INSERT IGNORE INTO systempreferences( `variable`, `value`, `options`, `explanation`, `type` )
42
            VALUES
43
              ( 'BundleLostValue', $lost_val, '', 'Sets the LOST AV value that represents "Missing from bundle" as a lost value', 'Free' ),
44
              ( 'BundleNotLoanValue', $nfl_val, '', 'Sets the NOT_LOAN AV value that represents "Added to bundle" as a not for loan value', 'Free')
45
        });
46
        say $out "System preferences added and set";
47
48
        if( index_exists( 'return_claims', 'issue_id' ) ) {
49
            $dbh->do(q{
50
                ALTER TABLE return_claims DROP INDEX issue_id
51
            });
52
            say $out "Dropped unique constraint on issue_id in return_claims";
53
        }
54
    }
55
}
(-)a/installer/data/mysql/en/optional/auth_val.yml (+8 lines)
Lines 106-111 tables: Link Here
106
          authorised_value: "4"
106
          authorised_value: "4"
107
          lib : "Missing"
107
          lib : "Missing"
108
108
109
        - category: "LOST"
110
          authorised_value: "5"
111
          lib : "Missing from bundle"
112
109
        # damaged status of an item
113
        # damaged status of an item
110
        - category: "DAMAGED"
114
        - category: "DAMAGED"
111
          authorised_value: "1"
115
          authorised_value: "1"
Lines 183-188 tables: Link Here
183
          authorised_value: "2"
187
          authorised_value: "2"
184
          lib: "Staff Collection"
188
          lib: "Staff Collection"
185
189
190
        - category: "NOT_LOAN"
191
          authorised_value: "3"
192
          lib: "Added to bundle"
193
186
        # restricted status of an item,linked to items.restricted
194
        # restricted status of an item,linked to items.restricted
187
        - category: "RESTRICTED"
195
        - category: "RESTRICTED"
188
          authorised_value: "1"
196
          authorised_value: "1"
(-)a/installer/data/mysql/kohastructure.sql (-1 / +17 lines)
Lines 3173-3178 CREATE TABLE `items` ( Link Here
3173
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3173
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3174
/*!40101 SET character_set_client = @saved_cs_client */;
3174
/*!40101 SET character_set_client = @saved_cs_client */;
3175
3175
3176
--
3177
-- Table structure for table item_bundles
3178
--
3179
3180
DROP TABLE IF EXISTS `item_bundles`;
3181
/*!40101 SET @saved_cs_client     = @@character_set_client */;
3182
/*!40101 SET character_set_client = utf8 */;
3183
CREATE TABLE `item_bundles` (
3184
  `item` int(11) NOT NULL,
3185
  `host` int(11) NOT NULL,
3186
  PRIMARY KEY (`host`, `item`),
3187
  UNIQUE KEY `item_bundles_uniq_1` (`item`),
3188
  CONSTRAINT `item_bundles_ibfk_1` FOREIGN KEY (`item`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
3189
  CONSTRAINT `item_bundles_ibfk_2` FOREIGN KEY (`host`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE
3190
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3191
/*!40101 SET character_set_client = @saved_cs_client */;
3192
3176
--
3193
--
3177
-- Table structure for table `items_last_borrower`
3194
-- Table structure for table `items_last_borrower`
3178
--
3195
--
Lines 4497-4503 CREATE TABLE `return_claims` ( Link Here
4497
  `resolved_on` timestamp NULL DEFAULT NULL COMMENT 'Time and date the claim was resolved',
4514
  `resolved_on` timestamp NULL DEFAULT NULL COMMENT 'Time and date the claim was resolved',
4498
  `resolved_by` int(11) DEFAULT NULL COMMENT 'ID of the staff member that resolved the claim',
4515
  `resolved_by` int(11) DEFAULT NULL COMMENT 'ID of the staff member that resolved the claim',
4499
  PRIMARY KEY (`id`),
4516
  PRIMARY KEY (`id`),
4500
  UNIQUE KEY `issue_id` (`issue_id`),
4501
  KEY `itemnumber` (`itemnumber`),
4517
  KEY `itemnumber` (`itemnumber`),
4502
  KEY `rc_borrowers_ibfk` (`borrowernumber`),
4518
  KEY `rc_borrowers_ibfk` (`borrowernumber`),
4503
  KEY `rc_created_by_ibfk` (`created_by`),
4519
  KEY `rc_created_by_ibfk` (`created_by`),
(-)a/installer/data/mysql/mandatory/sysprefs.sql (+2 lines)
Lines 115-120 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
115
('BorrowerUnwantedField','',NULL,'Name the fields you don\'t need to store for a patron\'s account','free'),
115
('BorrowerUnwantedField','',NULL,'Name the fields you don\'t need to store for a patron\'s account','free'),
116
('BranchTransferLimitsType','ccode','itemtype|ccode','When using branch transfer limits, choose whether to limit by itemtype or collection code.','Choice'),
116
('BranchTransferLimitsType','ccode','itemtype|ccode','When using branch transfer limits, choose whether to limit by itemtype or collection code.','Choice'),
117
('BrowseResultSelection','0',NULL,'Enable/Disable browsing search results fromt the bibliographic record detail page in staff interface','YesNo'),
117
('BrowseResultSelection','0',NULL,'Enable/Disable browsing search results fromt the bibliographic record detail page in staff interface','YesNo'),
118
('BundleLostValue','5',NULL,'Sets the LOST AV value that represents "Missing from bundle" as a lost value','Free'),
119
('BundleNotLoanValue','3',NULL,'Sets the NOT_LOAN AV value that represents "Added to bundle" as a not for loan value','Free'),
118
('CalculateFinesOnReturn','1','','Switch to control if overdue fines are calculated on return or not','YesNo'),
120
('CalculateFinesOnReturn','1','','Switch to control if overdue fines are calculated on return or not','YesNo'),
119
('CalculateFinesOnBackdate','1','','Switch to control if overdue fines are calculated on return when backdating','YesNo'),
121
('CalculateFinesOnBackdate','1','','Switch to control if overdue fines are calculated on return when backdating','YesNo'),
120
('CalendarFirstDayOfWeek','0','0|1|2|3|4|5|6','Select the first day of week to use in the calendar.','Choice'),
122
('CalendarFirstDayOfWeek','0','0|1|2|3|4|5|6','Select the first day of week to use in the calendar.','Choice'),
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/_tables.scss (-1 / +1 lines)
Lines 359-365 tbody { Link Here
359
                border-right: 1px solid $table-border-color;
359
                border-right: 1px solid $table-border-color;
360
            }
360
            }
361
        }
361
        }
362
        &:nth-child(odd):not(.dtrg-group):not(.active) {
362
        &:nth-child(odd):not(.dtrg-group):not(.active):not(.ok) {
363
            td {
363
            td {
364
                &:not(.bg-danger):not(.bg-warning):not(.bg-info):not(.bg-success):not(.bg-primary) {
364
                &:not(.bg-danger):not(.bg-warning):not(.bg-info):not(.bg-success):not(.bg-primary) {
365
                    background-color: $table-odd-row;
365
                    background-color: $table-odd-row;
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+14 lines)
Lines 1973-1978 i { Link Here
1973
    font-style: italic;
1973
    font-style: italic;
1974
}
1974
}
1975
1975
1976
// style for bundled detail in catalogsearch
1977
.bundled {
1978
    display: block;
1979
    font-style: italic;
1980
}
1981
1976
#menu {
1982
#menu {
1977
    border-right: 1px solid #B9D8D9;
1983
    border-right: 1px solid #B9D8D9;
1978
    margin-right: .5em;
1984
    margin-right: .5em;
Lines 2355-2360 td { Link Here
2355
    display: block;
2361
    display: block;
2356
}
2362
}
2357
2363
2364
.bundled {
2365
    display: block;
2366
}
2367
2368
td.bundle {
2369
    background-color: #FFC !important;
2370
}
2371
2358
.datedue {
2372
.datedue {
2359
    color: #999;
2373
    color: #999;
2360
    display: block;
2374
    display: block;
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/html_helpers.inc (+2 lines)
Lines 115-120 Link Here
115
                            [% ELSE %]
115
                            [% ELSE %]
116
                                [% IF subfield.IS_LOST_AV && Koha.Preference("ClaimReturnedLostValue") && aval == Koha.Preference("ClaimReturnedLostValue") %]
116
                                [% IF subfield.IS_LOST_AV && Koha.Preference("ClaimReturnedLostValue") && aval == Koha.Preference("ClaimReturnedLostValue") %]
117
                                    <option disabled="disabled" value="[%- aval | html -%]" title="Return claims must be processed from the patron details page">[%- mv.labels.$aval | html -%]</option>
117
                                    <option disabled="disabled" value="[%- aval | html -%]" title="Return claims must be processed from the patron details page">[%- mv.labels.$aval | html -%]</option>
118
                                [% ELSIF subfield.IS_LOST_AV && Koha.Preference("BundleLostValue") && aval == Koha.Preference("BundleLostValue") %]
119
                                    <option disabled="disabled" value="[%- aval | html -%]" title="Bundle losses are set at checkin automatically">[%- mv.labels.$aval | html -%]</option>
118
                                [%  ELSE %]
120
                                [%  ELSE %]
119
                                    <option value="[%- aval | html -%]">[%- mv.labels.$aval | html -%]</option>
121
                                    <option value="[%- aval | html -%]">[%- mv.labels.$aval | html -%]</option>
120
                                [% END %]
122
                                [% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js-biblio-format.inc (+64 lines)
Line 0 Link Here
1
<script>
2
    (function() {
3
        /**
4
         * Format the biblio response from a Koha RESTful API request.
5
         * @param  {Object}  biblio  The biblio json object as returned from the Koha RESTful API
6
         * @param  {Object}  config  A configuration object
7
         *                           Valid keys are: `link`
8
         * @return {string}          The formatted HTML string
9
         */
10
        window.$biblio_to_html = function(biblio, config) {
11
12
            if (biblio === undefined) {
13
                return ''; // empty string for no biblio
14
            }
15
16
            var title = '<span class="biblio-title">';
17
            if (biblio.title != null && biblio.title != '') {
18
                title += escape_str(biblio.title);
19
            } else {
20
                title += __("No title");
21
            }
22
            title += '</span>';
23
24
            // add subtitle
25
            if (biblio.subtitle != null && biblio.subtitle != '') {
26
                title += '<span class="biblio-subtitle">' + escape_str(biblio.subtitle) + '</span>';
27
            }
28
29
            // set title as link
30
            if (config && config.link) {
31
                if (config.link === 'marcdetail') {
32
                    title = '<a href="/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=' + encodeURIComponent(biblio.biblio_id) + '" class="title">' + title + '</a>';
33
                } else if (config.link === 'labeled_marc') {
34
                    title = '<a href="/cgi-bin/koha/catalogue/labeledMARCdetail.pl?biblionumber=' + encodeURIComponent(biblio.biblio_id) + '" class="title">' + title + '</a>';
35
                } else if (config.link === 'isbd') {
36
                    title = '<a href="/cgi-bin/koha/catalogue/ISBDdetail.pl?biblionumber=' + encodeURIComponent(biblio.biblio_id) + '" class="title">' + title + '</a>';
37
                } else {
38
                    title = '<a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=' + encodeURIComponent(biblio.biblio_id) + '" class="title">' + title + '</a>';
39
                }
40
            }
41
42
            // add medium
43
            if (biblio.medium != null && biblio.medium != '') {
44
                title += '<span class="biblio-medium">' + escape_str(biblio.medium) + '</span>';
45
            }
46
47
            // add part numbers/names
48
            let part_numbers = (typeof biblio.part_number === 'string') ? biblio.part_number.split("|") : [];
49
            let part_names = (typeof biblio.part_name === 'string') ? biblio.part_name.split("|") : [];
50
            let i = 0;
51
            while (part_numbers[i] || part_names[i]) {
52
                if (part_numbers[i]) {
53
                    title += '<span class="part-number">' + escape_str(part_numbers[i]) + '</span>';
54
                }
55
                if (part_names[i]) {
56
                    title += '<span class="part-name">' + escape_str(part_name[i]) + '</span>';
57
                }
58
                i++;
59
            }
60
61
            return title;
62
        };
63
    })();
64
</script>
(-)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 for [% item.barcode | html %]</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/circulation.pref (+11 lines)
Lines 1284-1289 Circulation: Link Here
1284
            - pref: ArticleRequestsSupportedFormats
1284
            - pref: ArticleRequestsSupportedFormats
1285
            - "(Valid choices are currently: PHOTOCOPY and SCAN. Separate the supported formats by a vertical bar. The first listed format is selected by default when you request via the OPAC.)"
1285
            - "(Valid choices are currently: PHOTOCOPY and SCAN. Separate the supported formats by a vertical bar. The first listed format is selected by default when you request via the OPAC.)"
1286
1286
1287
1288
    Item bundles:
1289
        -
1290
            - Use the LOST authorised value
1291
            - pref: BundleLostValue
1292
            - to represent 'missing from bundle' at return.
1293
        -
1294
            - Use the NOT_LOAN authorised value
1295
            - pref: BundleNotLoanValue
1296
            - to represent 'added to bundle' when an item is attached to bundle.
1297
1287
    Return claims:
1298
    Return claims:
1288
        -
1299
        -
1289
            - When marking a checkout as "claims returned",
1300
            - When marking a checkout as "claims returned",
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt (-2 / +368 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 %]" data-duedate="[% item.datedue | 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 ([% item.bundled | html %]|[% item.bundled_lost | html %])</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="addToBundleModal" tabindex="-1" role="dialog" aria-labelledby="addToBundleLabel">
1235
        <form id="addToBundleForm" 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="addToBundleLabel">Add to bundle</h3>
1241
                    </div>
1242
                    <div class="modal-body">
1243
                        <div id="addResult"></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
1263
    <div class="modal" id="removeFromBundleModal" tabindex="-1" role="dialog" aria-labelledby="removeFromBundleLabel">
1264
        <form id="removeFromBundleForm" action="">
1265
            <div class="modal-dialog" role="document">
1266
                <div class="modal-content">
1267
                    <div class="modal-header">
1268
                        <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
1269
                        <h3 id="removeFromBundleLabel">Remove from bundle</h3>
1270
                    </div>
1271
                    <div class="modal-body">
1272
                        <div id="removeResult"></div>
1273
                        <fieldset class="rows">
1274
                            <ol>
1275
                                <li>
1276
                                    <label class="required" for="external_id">Item barcode: </label>
1277
                                    <input type="text" id="rm_external_id" name="external_id" required="required">
1278
                                    <span class="required">Required</span>
1279
                                </li>
1280
                            </ol>
1281
                        </fieldset>
1282
                    </div>
1283
                    <div class="modal-footer">
1284
                        <button type="submit" class="btn btn-default">Submit</button>
1285
                        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
1286
                    </div>
1287
                </div>
1288
            </div>
1289
        </form>
1290
    </div>
1291
    [% END %]
1292
1225
[% MACRO jsinclude BLOCK %]
1293
[% MACRO jsinclude BLOCK %]
1226
    [% INCLUDE 'catalog-strings.inc' %]
1294
    [% INCLUDE 'catalog-strings.inc' %]
1227
    [% Asset.js("js/catalog.js") | $raw %]
1295
    [% 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' %]
1596
    [% INCLUDE 'datatables.inc' %]
1529
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1597
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1530
    [% INCLUDE 'columns_settings.inc' %]
1598
    [% INCLUDE 'columns_settings.inc' %]
1599
    [% INCLUDE 'js-date-format.inc' %]
1600
    [% INCLUDE 'js-patron-format.inc' %]
1601
    [% INCLUDE 'js-biblio-format.inc' %]
1531
    [% Asset.js("js/browser.js") | $raw %]
1602
    [% Asset.js("js/browser.js") | $raw %]
1532
    [% Asset.js("js/table_filters.js") | $raw %]
1603
    [% Asset.js("js/table_filters.js") | $raw %]
1533
    <script>
1604
    <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));
1606
        browser = KOHA.browser('[% searchid | html %]', parseInt(biblionumber, 10));
1536
        browser.show();
1607
        browser.show();
1537
1608
1609
        [% IF bundlesEnabled %]
1610
        var bundle_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail','bundle_tables','json') | $raw %];
1611
        var bundle_lost_value = [% Koha.Preference('BundleLostValue') | html %];
1612
        [% END %]
1538
        $(document).ready(function() {
1613
        $(document).ready(function() {
1614
1615
            [% IF bundlesEnabled %] // Bundle handling
1616
            function createChild ( row, itemnumber, duedate ) {
1617
1618
                // Toolbar
1619
                var bundle_toolbar = $('<div id="toolbar" class="btn-toolbar"></div>');
1620
                bundle_toolbar.append('<a class="btn btn-default" data-toggle="modal" data-target="#addToBundleModal" data-item="' + itemnumber + '"><i class="fa fa-plus"></i> ' + _("Add to bundle") + '</a>');
1621
                bundle_toolbar.append('<a class="btn btn-default" data-toggle="modal" data-target="#removeFromBundleModal" data-item="' + itemnumber + '"><i class="fa fa-minus"></i> ' + _("Remove from bundle") + '</a>');
1622
1623
                // Disable management if there's a duedate
1624
                if(duedate) {
1625
                    bundle_toolbar.children('.btn').addClass("disabled");
1626
                }
1627
1628
                // This is the table we'll convert into a DataTable
1629
                var bundles_table = $('<table class="display tbundle" data-itemnumber="'+itemnumber+'" id="bundle_table_'+itemnumber+'" width="100%"/>');
1630
1631
                // Display it the child row
1632
                row.child( bundle_toolbar.add(bundles_table), 'bundle' ).show();
1633
1634
                // Initialise as a DataTable
1635
                var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
1636
                var bundle_table = bundles_table.kohaTable({
1637
                    "ajax": {
1638
                        "url": bundle_table_url
1639
                    },
1640
                    "header_filter": false,
1641
                    "embed": [
1642
                        "biblio",
1643
                        "return_claim.patron"
1644
                    ],
1645
                    "order": [[ 1, "asc" ]],
1646
                    "columnDefs": [ {
1647
                        "targets": [0,1,2,3],
1648
                        "render": function (data, type, row, meta) {
1649
                            if ( data && type == 'display' ) {
1650
                                return data.escapeHtml();
1651
                            }
1652
                            return data;
1653
                        }
1654
                    } ],
1655
                    "columns": [
1656
                        {
1657
                            "data": "biblio.title:biblio.subtitle:biblio.medium",
1658
                            "title": _("Title"),
1659
                            "searchable": true,
1660
                            "orderable": true,
1661
                            "render": function(data, type, row, meta) {
1662
                                return $biblio_to_html(row.biblio, { link: 1 });
1663
                            }
1664
                        },
1665
                        {
1666
                            "data": "biblio.author",
1667
                            "title": _("Author"),
1668
                            "searchable": true,
1669
                            "orderable": true,
1670
                        },
1671
                        {
1672
                            "data": "callnumber",
1673
                            "title": _("Callnumber"),
1674
                            "searchable": true,
1675
                            "orderable": true,
1676
                        },
1677
                        {
1678
                            "data": "external_id",
1679
                            "title": _("Barcode"),
1680
                            "searchable": true,
1681
                            "orderable": true,
1682
                        },
1683
                        {
1684
                            "data": "lost_status:last_seen_date:return_claim.patron",
1685
                            "title": _("Status"),
1686
                            "searchable": false,
1687
                            "orderable": true,
1688
                            "render": function(data, type, row, meta) {
1689
                                if ( row.lost_status == bundle_lost_value ) {
1690
                                    let out = '<span class="lost">' + _("Last seen") + ': ' + $date(row.last_seen_date) + '</span>';
1691
                                    if ( row.return_claim ) {
1692
                                        out = out + '<span class="claims_return">' + _("Claims returned by") + ': ' + $patron_to_html( row.return_claim.patron, { display_cardnumber: false, url: true } ) + '</span>';
1693
                                    }
1694
                                    return out;
1695
                                }
1696
                                else if ( row.lost_status !== 0 ) {
1697
                                    return '<span class="lost">' + _("Lost") + ': ' + row.lost_status + '</span>';
1698
                                }
1699
                                return '<span class="available">' + _("Present") + '</span>';
1700
                            }
1701
                        },
1702
                        {
1703
                            "data": function( row, type, val, meta ) {
1704
                                var result;
1705
                                if (duedate) {
1706
                                    result = '<button class="btn btn-default btn-xs remove disabled" role="button" data-itemnumber="'+row.item_id+'"><i class="fa fa-minus" aria-hidden="true"></i> '+_("Remove")+'</button>\n';
1707
                                } else {
1708
                                    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';
1709
                                }
1710
                                return result;
1711
                            },
1712
                            "title": _("Actions"),
1713
                            "searchable": false,
1714
                            "orderable": false,
1715
                            "class": "noExport"
1716
                        }
1717
                    ]
1718
                }, bundle_settings, 1);
1719
                $(".tbundle").on("click", ".remove:not(.disabled)", function(){
1720
                    var bundle_table = $(this).closest('table');
1721
                    var host_itemnumber = bundle_table.data('itemnumber');
1722
                    var component_itemnumber = $(this).data('itemnumber');
1723
                    var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/" + component_itemnumber;
1724
                    $.ajax({
1725
                        type: "DELETE",
1726
                        url: unlink_item_url,
1727
                        success: function(){
1728
                            bundle_table.DataTable({ 'retrieve': true }).draw(false);
1729
                        }
1730
                    });
1731
                });
1732
1733
                return;
1734
            }
1735
1736
            var bundle_changed;
1737
            var bundle_form_active;
1738
            $("#addToBundleModal").on("shown.bs.modal", function(e){
1739
                var button = $(e.relatedTarget);
1740
                var item_id = button.data('item');
1741
                $("#addResult").replaceWith('<div id="addResult"></div>');
1742
                $("#addToBundleForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items');
1743
                $("#external_id").focus();
1744
                bundle_changed = 0;
1745
                bundle_form_active = item_id;
1746
            });
1747
1748
            $("#addToBundleForm").submit(function(event) {
1749
1750
                  /* stop form from submitting normally */
1751
                  event.preventDefault();
1752
1753
                  /* get the action attribute from the <form action=""> element */
1754
                  var $form = $(this),
1755
                  url = $form.attr('action');
1756
1757
                  /* Send the data using post with external_id */
1758
                  var posting = $.post({
1759
                      url: url,
1760
                      data: JSON.stringify({ external_id: $('#external_id').val()}),
1761
                      contentType: "application/json; charset=utf-8",
1762
                      dataType: "json"
1763
                  });
1764
1765
                  /* Report the results */
1766
                  posting.done(function(data) {
1767
                      var barcode = $('#external_id').val();
1768
                      $('#addResult').replaceWith('<div id="addResult" class="alert alert-success">'+_("Success: Added '%s'").format(barcode)+'</div>');
1769
                      $('#external_id').val('').focus();
1770
                      bundle_changed = 1;
1771
                  });
1772
                  posting.fail(function(data) {
1773
                      var barcode = $('#external_id').val();
1774
                      if ( data.status === 409 ) {
1775
                          var response = data.responseJSON;
1776
                          if ( response.key === "PRIMARY" ) {
1777
                              $('#addResult').replaceWith('<div id="addResult" class="alert alert-warning">'+_("Warning: Item '%s' already attached").format(barcode)+'</div>');
1778
                          } else {
1779
                              $('#addResult').replaceWith('<div id="addResult" class="alert alert-danger">'+_("Failure: Item '%s' belongs to another bundle").format(barcode)+'</div>');
1780
                          }
1781
                      } else if ( data.status === 404 ) {
1782
                          $('#addResult').replaceWith('<div id="addResult" class="alert alert-danger">'+_("Failure: Item '%s' not found").format(barcode)+'</div>');
1783
                      } else {
1784
                          $('#addResult').replaceWith('<div id="addResult" class="alert alert-danger">'+_("Failure: Check the logs for details")+'</div>');
1785
                      }
1786
                      $('#external_id').val('').focus();
1787
                  });
1788
            });
1789
1790
            $("#addToBundleModal").on("hidden.bs.modal", function(e){
1791
                if ( bundle_changed ) {
1792
                    $('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload();
1793
                }
1794
                bundle_form_active = 0;
1795
                bundle_changed = 0;
1796
            });
1797
1798
            $("#removeFromBundleModal").on("shown.bs.modal", function(e){
1799
                var button = $(e.relatedTarget);
1800
                var item_id = button.data('item');
1801
                $("#removeResult").replaceWith('<div id="removeResult"></div>');
1802
                $("#removeFromBundleForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items/');
1803
                $("#rm_external_id").focus();
1804
                bundle_changed = 0;
1805
                bundle_form_active = item_id;
1806
            });
1807
1808
            $("#removeFromBundleForm").submit(function(event) {
1809
1810
                /* stop form from submitting normally */
1811
                event.preventDefault();
1812
1813
                /* get the action attribute from the <form action=""> element */
1814
                var $form = $(this),
1815
                url = $form.attr('action');
1816
1817
                var barcode = $('#rm_external_id').val();
1818
1819
                /* Fetch itemnumber using rm_external_id */
1820
                var itemReq = $.get('/api/v1/items', { q: JSON.stringify({
1821
                    external_id: barcode
1822
                }) }, null, "json");
1823
1824
                var itemnumber;
1825
                itemReq.done(function(data) {
1826
                    if (data.length === 1) {
1827
                        itemnumber = data[0].item_id;
1828
1829
                        /* Remove link using fetch itemnumber */
1830
                        var deleteReq = $.ajax( url + itemnumber, {
1831
                            type : 'DELETE'
1832
                        });
1833
1834
                        /* Report the results */
1835
                        deleteReq.done(function(data) {
1836
                            var barcode = $('#rm_external_id').val();
1837
                            $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-success">'+_("Success: Removed '%s'").format(barcode)+'</div>');
1838
                            $('#rm_external_id').val('').focus();
1839
                            bundle_changed = 1;
1840
                        });
1841
                        deleteReq.fail(function(data) {
1842
                            var barcode = $('#rm_external_id').val();
1843
                            if ( data.status === 409 ) {
1844
                                var response = data.responseJSON;
1845
                                if ( response.key === "PRIMARY" ) {
1846
                                    $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-warning">'+_("Warning: Item '%s' already attached").format(barcode)+'</div>');
1847
                                } else {
1848
                                    $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-danger">'+_("Failure: Item '%s' belongs to another bundle").format(barcode)+'</div>');
1849
                                }
1850
                            } else if ( data.status === 404 ) {
1851
                                $('#addResult').replaceWith('<div id="addResult" class="alert alert-danger">'+_("Failure: Item '%s' not found").format(barcode)+'</div>');
1852
                            } else {
1853
                                $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-danger">'+_("Failure: Check the logs for details")+'</div>');
1854
                            }
1855
                            $('#rm_external_id').val('').focus();
1856
                        });
1857
                    } else {
1858
                        $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-danger">'+_("Failed: Barcode matched more than one item '%s'").format(barcode)+'</div>');
1859
                    }
1860
                });
1861
                itemReq.fail(function(data) {
1862
                     $('#removeResult').replaceWith('<div id="removeResult" class="alert alert-danger">'+_("Failed: Item not found '%s'").format(barcode)+'</div>');
1863
                    $('#rm_external_id').val('').focus();
1864
1865
                });
1866
            });
1867
1868
            $("#removeFromBundleModal").on("hidden.bs.modal", function(e){
1869
                if ( bundle_changed ) {
1870
                    $('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload();
1871
                }
1872
                bundle_form_active = 0;
1873
                bundle_changed = 0;
1874
            });
1875
            // End bundle handling
1876
            [% END %]
1877
1539
            var table_names = [ 'holdings_table', 'otherholdings_table' ];
1878
            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 %] ];
1879
            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 %]" ];
1880
            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>',
1890
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1552
                };
1891
                };
1553
                var table = KohaTable( table_name, dt_parameters, table_settings[index], 'with_filters' );
1892
                var table = KohaTable( table_name, dt_parameters, table_settings[index], 'with_filters' );
1893
1894
                [% IF bundlesEnabled %]
1895
                // Add event listener for opening and closing bundle details
1896
                $('#' + table_name + ' tbody').on('click', 'button.details-control', function () {
1897
                    var button = $(this);
1898
                    var tr = button.closest('tr');
1899
                    var dTable = button.closest('table').DataTable({ 'retrieve': true });
1900
1901
                    var itemnumber = tr.data('itemnumber');
1902
                    var duedate = tr.data('duedate');
1903
                    var row = dTable.row( tr );
1904
1905
                    if ( row.child.isShown() ) {
1906
                        // This row is already open - close it
1907
                        row.child.hide();
1908
                        tr.removeClass('shown');
1909
                        button.removeClass('active');
1910
                    }
1911
                    else {
1912
                        // Open this row
1913
                        createChild(row, itemnumber, duedate);
1914
                        tr.addClass('shown');
1915
                        button.addClass('active');
1916
                    }
1917
                } );
1918
                [% END %]
1554
            });
1919
            });
1555
1920
1556
            [% IF Koha.Preference('AcquisitionDetails') %]
1921
            [% 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"
1937
                    "sPaginationType": "full"
1573
                }));
1938
                }));
1574
            [% END %]
1939
            [% END %]
1940
1575
        });
1941
        });
1576
1942
1577
        [% IF (found1) %]
1943
        [% IF (found1) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt (-4 / +4 lines)
Lines 842-848 Link Here
842
                            </li>
842
                            </li>
843
                        [% END %]
843
                        [% END %]
844
844
845
                        [% IF Koha.Preference('ClaimReturnedLostValue') %]
845
                        [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %]
846
                            <li>
846
                            <li>
847
                                [% IF ( patron.return_claims.count ) %]
847
                                [% IF ( patron.return_claims.count ) %]
848
                                    <a href="#return-claims" id="return-claims-tab">
848
                                    <a href="#return-claims" id="return-claims-tab">
Lines 970-976 Link Here
970
                        </div>
970
                        </div>
971
                    [% END %]
971
                    [% END %]
972
972
973
                    [% IF Koha.Preference('ClaimReturnedLostValue') %]
973
                    [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %]
974
                        [% INCLUDE 'patron-return-claims.inc' %]
974
                        [% INCLUDE 'patron-return-claims.inc' %]
975
                    [% END %]
975
                    [% END %]
976
976
Lines 1006-1012 Link Here
1006
            </div> <!-- /.row -->
1006
            </div> <!-- /.row -->
1007
        </main>
1007
        </main>
1008
1008
1009
        [% IF Koha.Preference('ClaimReturnedLostValue') %]
1009
        [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %]
1010
            [% INCLUDE 'modals/resolve_return_claim.inc' %]
1010
            [% INCLUDE 'modals/resolve_return_claim.inc' %]
1011
        [% END %]
1011
        [% END %]
1012
1012
Lines 1054-1060 Link Here
1054
    </script>
1054
    </script>
1055
    [% Asset.js("js/pages/circulation.js") | $raw %]
1055
    [% Asset.js("js/pages/circulation.js") | $raw %]
1056
    [% Asset.js("js/checkouts.js") | $raw %]
1056
    [% Asset.js("js/checkouts.js") | $raw %]
1057
    [% IF Koha.Preference('ClaimReturnedLostValue') %]
1057
    [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %]
1058
        [% Asset.js("js/resolve_claim_modal.js") | $raw %]
1058
        [% Asset.js("js/resolve_claim_modal.js") | $raw %]
1059
    [% END %]
1059
    [% END %]
1060
    [% Asset.js("js/holds.js") | $raw %]
1060
    [% Asset.js("js/holds.js") | $raw %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt (+242 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>Bundle contents list updated</p>
226
                                    <p>
227
                                        <a class="btn btn-default btn-xs" role="button" data-toggle="modal" href="#bundleContentsModal"><i class="fa fa-eye" aria-hidden="true"></i> View updated contents list</a>
228
                                        <a class="btn btn-default btn-xs" role="button" data-toggle="modal" href="#bundleMissingModal"><i class="fa fa-eye" aria-hidden="true"></i> View list of missing items</a>
229
                                    </p>
230
                                </div>
231
                            [% END %]
232
233
                            <!-- Bundle contained unexpected items -->
234
                            [% IF unexpected_items %]
235
                                <div id="bundle_unexpected_items" class="dialog alert">
236
                                    <h3>Bundle had unexpected items</h3>
237
                                    <p>Please place the following items to one side</p>
238
                                    <ul>
239
                                    [% FOREACH unexpected_item IN unexpected_items %]
240
                                        <li>[% INCLUDE 'biblio-title.inc' biblio=unexpected_item.biblio %] - [% unexpected_item.barcode | html %]</li>
241
                                    [% END %]
242
                                    </ul>
243
                                </div>
244
                            [% END %]
245
246
                            <!-- Item checked in outside of bundle -->
247
                            [% IF InBundle %]
248
                                <div id="bundle_item_outside" class="dialog alert audio-alert-warning">
249
                                    <h3>Item belongs in bundle</h3>
250
                                    <p>This item belongs to a bundle: [% INCLUDE 'biblio-title.inc' biblio=InBundle.biblio %] - [% InBundle.barcode | html %]</p>
251
                                    <p><button class="btn btn-default btn-xs bundle_remove" role="button" data-itemnumber="[% itemnumber | uri %]" data-hostnumber="[% InBundle.itemnumber | uri %]"><i class="fa fa-minus"></i> Remove from bundle</button></p>
252
                                </div>
253
                            [% END %]
220
254
221
                            [% IF ( errmsgloop ) %]
255
                            [% IF ( errmsgloop ) %]
222
                                <div class="dialog alert audio-alert-warning">
256
                                <div class="dialog alert audio-alert-warning">
Lines 347-352 Link Here
347
                                        <p class="ret_checkinmsg">[% checkinmsg | html_line_break %]</p>
381
                                        <p class="ret_checkinmsg">[% checkinmsg | html_line_break %]</p>
348
                                    </div>
382
                                    </div>
349
                            [% END # /IF checkinmsg %]
383
                            [% END # /IF checkinmsg %]
384
385
                            [% IF bundle_items && !missing_items %]
386
                                <div class="dialog message">
387
                                    <h3>Bundle verified</h3>
388
                                    <p>The bundle content was verified</p>
389
                                    <p><a class="btn btn-default btn-xs" role="button" data-toggle="modal" href="#bundleContentsModal"><i class="fa fa-eye" aria-hidden="true"></i> View contents list</a></p>
390
                                </div>
391
                            [% END %]
350
                        [% END # /BLOCK all_checkin_messages %]
392
                        [% END # /BLOCK all_checkin_messages %]
351
393
352
                        [% IF needs_confirm %]
394
                        [% IF needs_confirm %]
Lines 381-386 Link Here
381
                            </div>
423
                            </div>
382
                        [% END %]
424
                        [% END %]
383
425
426
                        [% IF items_bundle_return_confirmation %]
427
                        <div id="bundle-needsconfirmation-modal" class="modal fade audio-alert-action block">
428
                            <div class="modal-dialog modal-wide">
429
                                <div class="modal-content">
430
                                    <form method="post">
431
                                        <div class="modal-header">
432
                                            <h3>Please confirm bundle contents for [% item.barcode | html %]</h3>
433
                                        </div>
434
                                        <div class="modal-body">
435
436
                                            <table class="table table-condensed table-bordered" id="items-bundle-contents-table">
437
                                                <thead>
438
                                                    <tr>
439
                                                        <th>Title</th>
440
                                                        <th>Author</th>
441
                                                        <th>Item type</th>
442
                                                        <th>Barcode</th>
443
                                                        [% IF !item.onloan %]
444
                                                        <th>Status</th>
445
                                                        [% END %]
446
                                                    </tr>
447
                                                </thead>
448
                                                <tbody>
449
                                                    [% FOREACH bundle_item IN item.bundle_items %]
450
                                                    [% IF !item.onloan %]
451
                                                    <tr data-barcode="[% bundle_item.barcode | html %]">
452
                                                        <td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio link = 1 %]</td>
453
                                                        <td>[% bundle_item.biblio.author | html %]</td>
454
                                                        <td>[% ItemTypes.GetDescription(bundle_item.itype) | html %]</td>
455
                                                        <td>[% bundle_item.barcode | html %]</td>
456
                                                        <td>
457
                                                            [% IF bundle_item.itemlost %]
458
                                                                [% itemlost_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.itemlost', authorised_value = bundle_item.itemlost }) %]
459
                                                                <span class="lost">[% itemlost_description | html %]</span>
460
                                                            [% ELSE %]
461
                                                                Present
462
                                                            [% END %]
463
                                                        </td>
464
                                                    </tr>
465
                                                    [% ELSIF !bundle_item.itemlost %]
466
                                                    <tr data-barcode="[% bundle_item.barcode | html %]">
467
                                                        <td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio link = 1 %]</td>
468
                                                        <td>[% bundle_item.biblio.author | html %]</td>
469
                                                        <td>[% ItemTypes.GetDescription(bundle_item.itype) | html %]</td>
470
                                                        <td>[% bundle_item.barcode | html %]</td>
471
                                                    </tr>
472
                                                    [% END %]
473
                                                    [% END %]
474
                                                </tbody>
475
                                            </table>
476
477
                                            <div class="form-group">
478
                                                <label for="verify-items-bundle-contents-barcodes">Barcodes <span id="verify-progress" class="pull-right" style="display: none"><span id="verified">0</span> of <span id="expected"></span> verified</span></label>
479
                                                <textarea autocomplete="off" id="verify-items-bundle-contents-barcodes" name="verify-items-bundle-contents-barcodes" class="form-control"></textarea>
480
                                                [% IF item.onloan %]
481
                                                <div class="help-block">Scan all barcodes of items found in the items bundle. If any items are missing, they will be marked as lost</div>
482
                                                [% ELSE %]
483
                                                <div class="help-block">Optionally scan all barcodes of items found in the items bundle to perform an inventory check. If any items are missing, they will be marked as lost</div>
484
                                                [% END %]
485
                                            </div>
486
487
                                            <div id="bundle-feedback" class="alert" style="display:none"></div>
488
489
                                        </div>
490
                                        <div class="modal-footer">
491
                                            <input type="hidden" name="barcode" value="[% item.barcode | html %]">
492
                                            <input type="hidden" name="confirm_items_bundle_return" value="1">
493
                                            [% FOREACH inputloo IN inputloop %]
494
                                            <input type="hidden" name="ri-[% inputloo.counter | html %]" value="[% inputloo.barcode | html %]" />
495
                                            <input type="hidden" name="dd-[% inputloo.counter | html %]" value="[% inputloo.duedate | html %]" />
496
                                            <input type="hidden" name="bn-[% inputloo.counter | html %]" value="[% inputloo.borrowernumber | html %]" />
497
                                            [% END %]
498
                                            [% IF item.onloan %]
499
                                            <button type="submit" class="btn btn-default"><i class="fa fa-check"></i> Confirm checkin and mark missing items as lost</button>
500
                                            [% ELSE %]
501
                                            <button type="submit" class="btn btn-default"><i class="fa fa-check"></i> Confirm inventory check and mark items as lost</button>
502
                                            [% END %]
503
                                            <button type="button" data-dismiss="modal" class="btn btn-default"><i class="fa fa-close"></i> Cancel</button>
504
                                        </div>
505
                                    </form>
506
                                </div>
507
                            </div>
508
                        </div>
509
                        [% END %]
510
384
                        [% IF wrongbranch %]
511
                        [% IF wrongbranch %]
385
                            <div id="wrong-branch-modal" class="modal fade audio-alert-action block">
512
                            <div id="wrong-branch-modal" class="modal fade audio-alert-action block">
386
                                <div class="modal-dialog">
513
                                <div class="modal-dialog">
Lines 1180-1185 Link Here
1180
        [% INCLUDE 'modals/resolve_return_claim.inc' %]
1307
        [% INCLUDE 'modals/resolve_return_claim.inc' %]
1181
    [% END %]
1308
    [% END %]
1182
1309
1310
    [% INCLUDE 'modals/bundle_contents.inc' %]
1311
1312
    [% IF ( missing_items ) %]
1313
    <!-- Bundle missing modal -->
1314
    <div class="modal printable" id="bundleMissingModal" tabindex="-1" role="dialog" aria-labelledby="bundleMissingLabel">
1315
        <div class="modal-dialog" role="document">
1316
            <div class="modal-content">
1317
                <div class="modal-header">
1318
                    <button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
1319
                    <h4 class="modal-title" id="bundleMissingLabel">Items missing from bundle at checking for [% item.barcode | html %]</h4>
1320
                </div>
1321
                <div class="modal-body">
1322
                    <table style="width:100%">
1323
                        <thead>
1324
                            <tr>
1325
                                <th>Barcode</th>
1326
                                <th>Title</th>
1327
                            </tr>
1328
                        </thead>
1329
                        <tbody>
1330
                        [% FOREACH bundle_item IN missing_items %]
1331
                            <tr>
1332
                                <td>[% bundle_item.barcode | html %]</td>
1333
                                <td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio %]</td>
1334
                            </tr>
1335
                        [% END %]
1336
                        </tbody>
1337
                        <tfoot>
1338
                        </tfoot>
1339
                    </table>
1340
                </div> <!-- /.modal-body -->
1341
                <div class="modal-footer">
1342
                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
1343
                    <button type="button" class="printModal btn btn-primary"><i class="fa fa-print"></i> Print</button>
1344
                </div> <!-- /.modal-footer -->
1345
            </div> <!-- /.modal-content -->
1346
        </div> <!-- /.modal-dialog -->
1347
    </div> <!-- /#bundleMissingModal -->
1348
    [% END %]
1349
1183
[% MACRO jsinclude BLOCK %]
1350
[% MACRO jsinclude BLOCK %]
1184
    [% INCLUDE 'datatables.inc' %]
1351
    [% INCLUDE 'datatables.inc' %]
1185
    [% INCLUDE 'columns_settings.inc' %]
1352
    [% INCLUDE 'columns_settings.inc' %]
Lines 1364-1371 Link Here
1364
                window.open("/cgi-bin/koha/members/printslip.pl?borrowernumber=" + borrowernumber + "&amp;print=checkinslip", "printwindow");
1531
                window.open("/cgi-bin/koha/members/printslip.pl?borrowernumber=" + borrowernumber + "&amp;print=checkinslip", "printwindow");
1365
            });
1532
            });
1366
1533
1534
            // item bundles
1535
            $('#verify-items-bundle-contents-barcodes').on('input', function (ev) {
1536
                let char = ev.target.value.slice(-1);
1537
                if ( char.match(/\n/) ) {
1538
                    const barcodes = ev.target.value.split('\n').map(function(s) { return s.trim().toUpperCase() });
1539
                    const expected = [];
1540
                    let found = 0;
1541
                    $('#items-bundle-contents-table tbody > tr').each(function () {
1542
                        const barcode = this.getAttribute('data-barcode').toUpperCase();
1543
                        expected.push(barcode);
1544
                        if (barcodes.includes(barcode)) {
1545
                            this.classList.add('ok');
1546
                            found++;
1547
                        } else {
1548
                            this.classList.remove('ok');
1549
                        }
1550
                    });
1551
                    const last = barcodes[barcodes.length -2];
1552
                    const feedback = $('#bundle-feedback');
1553
                    let string;
1554
                    if ( !expected.includes(last) ) {
1555
                        feedback.fadeOut(100, function(){
1556
                            string = _("Unexpected: ") +last;
1557
                            feedback.addClass('alert-danger').removeClass('alert-success').html(string).fadeIn(100);
1558
                        });
1559
                    } else {
1560
                        feedback.fadeOut(100, function(){
1561
                            string = _("Verified: ")+last;
1562
                            feedback.addClass('alert-success').removeClass('alert-danger').html(string).fadeIn(100);
1563
                        });
1564
                    }
1565
                    $('#verify-progress').show();
1566
                    $('#verified').text(found);
1567
                    $('#expected').text(expected.length);
1568
                }
1569
            });
1570
1571
            $('.bundle_remove').on('click', function() {
1572
                var component_itemnumber = $(this).data('itemnumber');
1573
                var host_itemnumber = $(this).data('hostnumber');
1574
                var alert = $(this).closest('div');
1575
                var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/item/" + component_itemnumber;
1576
                $.ajax({
1577
                    type: "DELETE",
1578
                    url: unlink_item_url,
1579
                    success: function(){
1580
                        alert.remove();
1581
                    }
1582
                });
1583
            });
1584
1585
            $('#items-bundle-contents-table').dataTable($.extend(true, {}, dataTablesDefaults, {
1586
                "bFilter": false,
1587
                "bPaginate": false,
1588
                "bInfo": false,
1589
                "order": [[ 1, 'asc' ], [ 0, 'asc' ]]
1590
            }));
1591
1592
            // print modals
1593
            $('.modal.printable').on('shown.bs.modal', function() {
1594
                $('.modal-dialog', this).addClass('focused');
1595
                $('body').addClass('modalprinter');
1596
1597
                if ($(this).hasClass('autoprint')) {
1598
                    window.print();
1599
                }
1600
            }).on('hidden.bs.modal', function() {
1601
                $('.modal-dialog', this).removeClass('focused');
1602
                $('body').removeClass('modalprinter');
1603
            });
1604
1605
            $('.printModal').click(function() {
1606
                window.print();
1607
            });
1367
        });
1608
        });
1368
    </script>
1609
    </script>
1610
1369
[% END %]
1611
[% END %]
1370
1612
1371
[% INCLUDE 'intranet-bottom.inc' %]
1613
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt (-1 / +1 lines)
Lines 671-677 Link Here
671
                                </li>
671
                                </li>
672
                            [% END %]
672
                            [% END %]
673
673
674
                            [% IF Koha.Preference('ClaimReturnedLostValue') %]
674
                            [% IF Koha.Preference('ClaimReturnedLostValue') || Koha.Preference('BundleLostValue') %]
675
                                <li>
675
                                <li>
676
                                    [% IF ( patron.return_claims.count ) %]
676
                                    [% IF ( patron.return_claims.count ) %]
677
                                        <a href="#return-claims" id="return-claims-tab">
677
                                        <a href="#return-claims" id="return-claims-tab">
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/item-status.inc (+4 lines)
Lines 76-81 Link Here
76
    <span class="item-status notforloan">Not for loan [% IF ( item.restrictedvalueopac ) %]<span class="restricted">([% item.restrictedvalueopac | html %])</span>[% END %]</span>
76
    <span class="item-status notforloan">Not for loan [% IF ( item.restrictedvalueopac ) %]<span class="restricted">([% item.restrictedvalueopac | html %])</span>[% END %]</span>
77
[% END %]
77
[% END %]
78
78
79
[% IF ( item.bundle_host ) %]
80
    <span class="bundled">In bundle: <a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% item.bundle_host.biblionumber | uri %]">[% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio %]</a></span>
81
[% END %]
82
79
[% IF ( item.damaged ) %]
83
[% IF ( item.damaged ) %]
80
    [% SET itemavailable = 0 %]
84
    [% SET itemavailable = 0 %]
81
    [% av_lib_include = AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged, opac => 1 ) %]
85
    [% av_lib_include = AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged, opac => 1 ) %]
(-)a/opac/opac-detail.pl (+5 lines)
Lines 766-771 if ( not $viewallitems and @items > $max_items_to_display ) { Link Here
766
        $itm->{cover_images} = $item->cover_images;
766
        $itm->{cover_images} = $item->cover_images;
767
    }
767
    }
768
768
769
    if ( $item->in_bundle ) {
770
        my $host = $item->bundle_host;
771
        $itm->{bundle_host} = $host;
772
    }
773
769
    my $itembranch = $itm->{$separatebranch};
774
    my $itembranch = $itm->{$separatebranch};
770
    if ($currentbranch and C4::Context->preference('OpacSeparateHoldings')) {
775
    if ($currentbranch and C4::Context->preference('OpacSeparateHoldings')) {
771
        if ($itembranch and $itembranch eq $currentbranch) {
776
        if ($itembranch and $itembranch eq $currentbranch) {
(-)a/t/db_dependent/Accounts.t (-1 / +15 lines)
Lines 555-572 subtest "C4::Accounts::chargelostitem tests" => sub { Link Here
555
    my $cli_itemnumber2 = $builder->build_sample_item({ itype => $itype_replace_no_fee->{itemtype} })->itemnumber;
555
    my $cli_itemnumber2 = $builder->build_sample_item({ itype => $itype_replace_no_fee->{itemtype} })->itemnumber;
556
    my $cli_itemnumber3 = $builder->build_sample_item({ itype => $itype_no_replace_fee->{itemtype} })->itemnumber;
556
    my $cli_itemnumber3 = $builder->build_sample_item({ itype => $itype_no_replace_fee->{itemtype} })->itemnumber;
557
    my $cli_itemnumber4 = $builder->build_sample_item({ itype => $itype_replace_fee->{itemtype} })->itemnumber;
557
    my $cli_itemnumber4 = $builder->build_sample_item({ itype => $itype_replace_fee->{itemtype} })->itemnumber;
558
    my $cli_itemnumber5 = $builder->build_sample_item({ itype => $itype_replace_fee->{itemtype} })->itemnumber;
559
    my $cli_bundle1     = $builder->build_sample_item({ itype => $itype_no_replace_no_fee->{itemtype} })->itemnumber;
560
    $schema->resultset('ItemBundle')->create( { host => $cli_bundle1, item => $cli_itemnumber5 } );
558
561
559
    my $cli_issue_id_1 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber1 } })->{issue_id};
562
    my $cli_issue_id_1 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber1 } })->{issue_id};
560
    my $cli_issue_id_2 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber2 } })->{issue_id};
563
    my $cli_issue_id_2 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber2 } })->{issue_id};
561
    my $cli_issue_id_3 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber3 } })->{issue_id};
564
    my $cli_issue_id_3 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber3 } })->{issue_id};
562
    my $cli_issue_id_4 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber4 } })->{issue_id};
565
    my $cli_issue_id_4 = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber4 } })->{issue_id};
563
    my $cli_issue_id_4X = undef;
566
    my $cli_issue_id_4X = undef;
567
    my $cli_bundle_issue = $builder->build({ source => 'Issue', value => { borrowernumber => $cli_borrowernumber, itemnumber => $cli_bundle1 } })->{issue_id};
564
568
565
    my $lostfine;
569
    my $lostfine;
566
    my $procfee;
570
    my $procfee;
567
571
568
    subtest "fee application tests" => sub {
572
    subtest "fee application tests" => sub {
569
        plan tests => 44;
573
        plan tests => 48;
570
574
571
        t::lib::Mocks::mock_preference('item-level_itypes', '1');
575
        t::lib::Mocks::mock_preference('item-level_itypes', '1');
572
        t::lib::Mocks::mock_preference('useDefaultReplacementCost', '0');
576
        t::lib::Mocks::mock_preference('useDefaultReplacementCost', '0');
Lines 694-699 subtest "C4::Accounts::chargelostitem tests" => sub { Link Here
694
        ok( $procfees->count == 2,  "Processing fee can be charged twice for the same item if they are distinct issue_id's");
698
        ok( $procfees->count == 2,  "Processing fee can be charged twice for the same item if they are distinct issue_id's");
695
        $lostfines->delete();
699
        $lostfines->delete();
696
        $procfees->delete();
700
        $procfees->delete();
701
702
        C4::Accounts::chargelostitem( $cli_borrowernumber, $cli_itemnumber5, 6.12, "Bundle");
703
        $lostfine = Koha::Account::Lines->find({ borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber5, debit_type_code => 'LOST' });
704
        $procfee  = Koha::Account::Lines->find({ borrowernumber => $cli_borrowernumber, itemnumber => $cli_itemnumber5, debit_type_code => 'PROCESSING' });
705
        is( $lostfine->amount, "6.120000", "Lost fine equals replacementcost when pref on and default set (Bundle)");
706
        is( $procfee->amount, "2.040000",  "Processing fee if processing fee (Bundle)");
707
        is( $lostfine->issue_id, $cli_bundle_issue, "Lost fine issue id matched to bundle issue");
708
        is( $procfee->issue_id, $cli_bundle_issue, "Processing fee issue id matched to bundle issue");
709
        $lostfine->delete();
710
        $procfee->delete();
697
    };
711
    };
698
712
699
    subtest "basic fields tests" => sub {
713
    subtest "basic fields tests" => sub {
(-)a/t/db_dependent/Circulation.t (+19 lines)
Lines 4298-4303 subtest 'AddReturn | recalls' => sub { Link Here
4298
    $recall1->set_cancelled;
4298
    $recall1->set_cancelled;
4299
};
4299
};
4300
4300
4301
subtest 'AddReturn | bundles' => sub {
4302
    plan tests => 1;
4303
4304
    my $schema = Koha::Database->schema;
4305
    $schema->storage->txn_begin;
4306
4307
    my $patron1 = $builder->build_object({ class => 'Koha::Patrons' });
4308
    my $host_item1 = $builder->build_sample_item;
4309
    my $bundle_item1 = $builder->build_sample_item;
4310
    $schema->resultset('ItemBundle')
4311
      ->create(
4312
        { host => $host_item1->itemnumber, item => $bundle_item1->itemnumber } );
4313
4314
    my ( $doreturn, $messages, $iteminfo, $borrowerinfo ) = AddReturn( $bundle_item1->barcode, $bundle_item1->homebranch );
4315
    is($messages->{InBundle}->id, $host_item1->id, 'AddReturn returns InBundle host item when item is part of a bundle');
4316
4317
    $schema->storage->txn_rollback;
4318
};
4319
4301
subtest 'AddRenewal and AddIssuingCharge tests' => sub {
4320
subtest 'AddRenewal and AddIssuingCharge tests' => sub {
4302
4321
4303
    plan tests => 13;
4322
    plan tests => 13;
(-)a/t/db_dependent/Koha/Item.t (-2 / +194 lines)
Lines 20-26 Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
use utf8;
21
use utf8;
22
22
23
use Test::More tests => 17;
23
use Test::More tests => 25;
24
use Test::Exception;
24
use Test::Exception;
25
use Test::MockModule;
25
use Test::MockModule;
26
26
Lines 41-46 use t::lib::Mocks; Link Here
41
my $schema  = Koha::Database->new->schema;
41
my $schema  = Koha::Database->new->schema;
42
my $builder = t::lib::TestBuilder->new;
42
my $builder = t::lib::TestBuilder->new;
43
43
44
subtest 'return_claims relationship' => sub {
45
    plan tests => 3;
46
47
    $schema->storage->txn_begin;
48
49
    my $biblio = $builder->build_sample_biblio();
50
    my $item   = $builder->build_sample_item({
51
        biblionumber => $biblio->biblionumber,
52
    });
53
    my $return_claims = $item->return_claims;
54
    is( ref($return_claims), 'Koha::Checkouts::ReturnClaims', 'return_claims returns a Koha::Checkouts::ReturnClaims object set' );
55
    is($item->return_claims->count, 0, "Empty Koha::Checkouts::ReturnClaims set returned if no return_claims");
56
    my $claim1 = $builder->build({ source => 'ReturnClaim', value => { itemnumber => $item->itemnumber }});
57
    my $claim2 = $builder->build({ source => 'ReturnClaim', value => { itemnumber => $item->itemnumber }});
58
59
    is($item->return_claims()->count,2,"Two ReturnClaims found for item");
60
61
    $schema->storage->txn_rollback;
62
};
63
64
subtest 'return_claim accessor' => sub {
65
    plan tests => 5;
66
67
    $schema->storage->txn_begin;
68
69
    my $biblio = $builder->build_sample_biblio();
70
    my $item   = $builder->build_sample_item({
71
        biblionumber => $biblio->biblionumber,
72
    });
73
    my $return_claim = $item->return_claim;
74
    is( $return_claim, undef, 'return_claim returned undefined if there are no claims for this item' );
75
76
    my $claim1 = $builder->build_object(
77
        {
78
            class => 'Koha::Checkouts::ReturnClaims',
79
            value => { itemnumber => $item->itemnumber, resolution => undef, created_on => dt_from_string()->subtract( minutes => 10 ) }
80
        }
81
    );
82
    my $claim2 = $builder->build_object(
83
        {
84
            class => 'Koha::Checkouts::ReturnClaims',
85
            value  => { itemnumber => $item->itemnumber, resolution => undef, created_on => dt_from_string()->subtract( minutes => 5 ) }
86
        }
87
    );
88
89
    $return_claim = $item->return_claim;
90
    is( ref($return_claim), 'Koha::Checkouts::ReturnClaim', 'return_claim returned a Koha::Checkouts::ReturnClaim object' );
91
    is( $return_claim->id, $claim2->id, 'return_claim returns the most recent unresolved claim');
92
93
    $claim2->resolution('test')->store();
94
    $return_claim = $item->return_claim;
95
    is( $return_claim->id, $claim1->id, 'return_claim returns the only unresolved claim');
96
97
    $claim1->resolution('test')->store();
98
    $return_claim = $item->return_claim;
99
    is( $return_claim, undef, 'return_claim returned undefined if there are no active claims for this item' );
100
101
    $schema->storage->txn_rollback;
102
};
103
44
subtest 'tracked_links relationship' => sub {
104
subtest 'tracked_links relationship' => sub {
45
    plan tests => 3;
105
    plan tests => 3;
46
106
Lines 57-62 subtest 'tracked_links relationship' => sub { Link Here
57
    is($item->tracked_links()->count,2,"Two tracked links found");
117
    is($item->tracked_links()->count,2,"Two tracked links found");
58
};
118
};
59
119
120
subtest 'is_bundle tests' => sub {
121
    plan tests => 2;
122
123
    $schema->storage->txn_begin;
124
125
    my $item   = $builder->build_sample_item();
126
127
    my $is_bundle = $item->is_bundle;
128
    is($is_bundle, 0, 'is_bundle returns 0 when there are no items attached');
129
130
    my $item2 = $builder->build_sample_item();
131
    $schema->resultset('ItemBundle')
132
      ->create( { host => $item->itemnumber, item => $item2->itemnumber } );
133
134
    $is_bundle = $item->is_bundle;
135
    is($is_bundle, 1, 'is_bundle returns 1 when there is at least one item attached');
136
137
    $schema->storage->txn_rollback;
138
};
139
140
subtest 'in_bundle tests' => sub {
141
    plan tests => 2;
142
143
    $schema->storage->txn_begin;
144
145
    my $item   = $builder->build_sample_item();
146
147
    my $in_bundle = $item->in_bundle;
148
    is($in_bundle, 0, 'in_bundle returns 0 when the item is not in a bundle');
149
150
    my $host_item = $builder->build_sample_item();
151
    $schema->resultset('ItemBundle')
152
      ->create( { host => $host_item->itemnumber, item => $item->itemnumber } );
153
154
    $in_bundle = $item->in_bundle;
155
    is($in_bundle, 1, 'in_bundle returns 1 when the item is in a bundle');
156
157
    $schema->storage->txn_rollback;
158
};
159
160
subtest 'bundle_items tests' => sub {
161
    plan tests => 3;
162
163
    $schema->storage->txn_begin;
164
165
    my $host_item = $builder->build_sample_item();
166
    my $bundle_items = $host_item->bundle_items;
167
    is( ref($bundle_items), 'Koha::Items',
168
        'bundle_items returns a Koha::Items object set' );
169
    is( $bundle_items->count, 0,
170
        'bundle_items set is empty when no items are bundled' );
171
172
    my $bundle_item1 = $builder->build_sample_item();
173
    my $bundle_item2 = $builder->build_sample_item();
174
    my $bundle_item3 = $builder->build_sample_item();
175
    $schema->resultset('ItemBundle')
176
      ->create(
177
        { host => $host_item->itemnumber, item => $bundle_item1->itemnumber } );
178
    $schema->resultset('ItemBundle')
179
      ->create(
180
        { host => $host_item->itemnumber, item => $bundle_item2->itemnumber } );
181
    $schema->resultset('ItemBundle')
182
      ->create(
183
        { host => $host_item->itemnumber, item => $bundle_item3->itemnumber } );
184
185
    $bundle_items = $host_item->bundle_items;
186
    is( $bundle_items->count, 3,
187
        'bundle_items returns all the bundled items in the set' );
188
189
    $schema->storage->txn_rollback;
190
};
191
192
subtest 'bundle_host tests' => sub {
193
    plan tests => 3;
194
195
    $schema->storage->txn_begin;
196
197
    my $host_item = $builder->build_sample_item();
198
    my $bundle_item1 = $builder->build_sample_item();
199
    my $bundle_item2 = $builder->build_sample_item();
200
    $schema->resultset('ItemBundle')
201
      ->create(
202
        { host => $host_item->itemnumber, item => $bundle_item2->itemnumber } );
203
204
    my $bundle_host = $bundle_item1->bundle_host;
205
    is( $bundle_host, undef, 'bundle_host returns undefined when the item it not part of a bundle');
206
    $bundle_host = $bundle_item2->bundle_host;
207
    is( ref($bundle_host), 'Koha::Item', 'bundle_host returns a Koha::Item object when the item is in a bundle');
208
    is( $bundle_host->id, $host_item->id, 'bundle_host returns the host item when called against an item in a bundle');
209
210
    $schema->storage->txn_rollback;
211
};
212
213
subtest 'add_to_bundle tests' => sub {
214
    plan tests => 3;
215
216
    $schema->storage->txn_begin;
217
218
    t::lib::Mocks::mock_preference( 'BundleNotLoanValue', 1 );
219
220
    my $host_item = $builder->build_sample_item();
221
    my $bundle_item1 = $builder->build_sample_item();
222
    my $bundle_item2 = $builder->build_sample_item();
223
224
    ok($host_item->add_to_bundle($bundle_item1), 'bundle_item1 added to bundle');
225
    is($bundle_item1->notforloan, 1, 'add_to_bundle sets notforloan to BundleNotLoanValue');
226
227
    throws_ok { $host_item->add_to_bundle($bundle_item1) }
228
    'Koha::Exceptions::Object::DuplicateID',
229
      'Exception thrown if you try to add the same item twice';
230
231
    $schema->storage->txn_rollback;
232
};
233
234
subtest 'remove_from_bundle tests' => sub {
235
    plan tests => 3;
236
237
    $schema->storage->txn_begin;
238
239
    my $host_item = $builder->build_sample_item();
240
    my $bundle_item1 = $builder->build_sample_item({ notforloan => 1 });
241
    $schema->resultset('ItemBundle')
242
      ->create(
243
        { host => $host_item->itemnumber, item => $bundle_item1->itemnumber } );
244
245
    is($bundle_item1->remove_from_bundle(), 1, 'remove_from_bundle returns 1 when item is removed from a bundle');
246
    is($bundle_item1->notforloan, 0, 'remove_from_bundle resets notforloan to 0');
247
    $bundle_item1 = $bundle_item1->get_from_storage;
248
    is($bundle_item1->remove_from_bundle(), 0, 'remove_from_bundle returns 0 when item is not in a bundle');
249
250
    $schema->storage->txn_rollback;
251
};
252
60
subtest 'hidden_in_opac() tests' => sub {
253
subtest 'hidden_in_opac() tests' => sub {
61
254
62
    plan tests => 4;
255
    plan tests => 4;
63
- 

Return to bug 28854