Bugzilla – Attachment 78394 Details for
Bug 11897
Stock Rotation for Koha
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11897: Stockrotation
Bug-11897-Stockrotation.patch (text/plain), 274.67 KB, created by
Martin Renvoize (ashimema)
on 2018-09-04 13:13:10 UTC
(
hide
)
Description:
Bug 11897: Stockrotation
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2018-09-04 13:13:10 UTC
Size:
274.67 KB
patch
obsolete
>From a6c286e490675fd9b336074ca2d0e0b2ce49c1b9 Mon Sep 17 00:00:00 2001 >From: Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >Date: Thu, 8 Dec 2016 18:32:04 +0100 >Subject: [PATCH] Bug 11897: Stockrotation > >The stock rotation feature adds a batch process to automate rotation of >catalgue items with a staff client page under tools to manage rotas/schedules. > >Once a rota is configured, and your staff user has the right permissions >to allocate items, then an additional tab will appear on biblio records >allowing the management of of which rota, if any, individual items belong >to. > >It also includes a cron script to process the items on a daily basis. > >Signed-off-by: Kathleen Milne <kathleen.milne@cne-siar.gov.uk> >--- > Koha/Item.pm | 49 +- > Koha/Library.pm | 46 ++ > Koha/REST/V1/Stage.pm | 60 ++ > Koha/StockRotationItem.pm | 273 +++++++++ > Koha/StockRotationItems.pm | 128 +++++ > Koha/StockRotationRota.pm | 182 ++++++ > Koha/StockRotationRotas.pm | 105 ++++ > Koha/StockRotationStage.pm | 419 ++++++++++++++ > Koha/StockRotationStages.pm | 90 +++ > Koha/Util/StockRotation.pm | 247 ++++++++ > api/v1/swagger/paths.json | 3 + > api/v1/swagger/paths/rotas.json | 79 +++ > catalogue/stockrotation.pl | 179 ++++++ > .../mysql/en/mandatory/sample_notices.sql | 3 +- > installer/data/mysql/kohastructure.sql | 72 +-- > installer/data/mysql/sysprefs.sql | 7 +- > .../prog/css/src/staff-global.scss | 121 ++++ > .../intranet-tmpl/prog/css/staff-global.css | 2 +- > .../prog/en/includes/biblio-view-menu.inc | 1 + > .../prog/en/includes/permissions.inc | 5 +- > .../en/includes/stockrotation-toolbar.inc | 12 + > .../prog/en/includes/tools-menu.inc | 5 + > .../en/modules/catalogue/stockrotation.tt | 171 ++++++ > .../prog/en/modules/tools/stockrotation.tt | 510 +++++++++++++++++ > .../prog/en/modules/tools/tools-home.tt | 7 + > .../prog/js/pages/stockrotation.js | 65 +++ > misc/cronjobs/stockrotation.pl | 528 +++++++++++++++++ > t/db_dependent/Items.t | 61 ++ > t/db_dependent/Koha/Libraries.t | 25 +- > t/db_dependent/StockRotationItems.t | 393 +++++++++++++ > t/db_dependent/StockRotationRotas.t | 175 ++++++ > t/db_dependent/StockRotationStages.t | 377 +++++++++++++ > t/db_dependent/api/v1/stockrotationstage.t | 172 ++++++ > tools/stockrotation.pl | 531 ++++++++++++++++++ > 34 files changed, 5057 insertions(+), 46 deletions(-) > create mode 100644 Koha/REST/V1/Stage.pm > create mode 100644 Koha/StockRotationItem.pm > create mode 100644 Koha/StockRotationItems.pm > create mode 100644 Koha/StockRotationRota.pm > create mode 100644 Koha/StockRotationRotas.pm > create mode 100644 Koha/StockRotationStage.pm > create mode 100644 Koha/StockRotationStages.pm > create mode 100644 Koha/Util/StockRotation.pm > create mode 100644 api/v1/swagger/paths/rotas.json > create mode 100755 catalogue/stockrotation.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js > create mode 100755 misc/cronjobs/stockrotation.pl > create mode 100644 t/db_dependent/StockRotationItems.t > create mode 100644 t/db_dependent/StockRotationRotas.t > create mode 100644 t/db_dependent/StockRotationStages.t > create mode 100644 t/db_dependent/api/v1/stockrotationstage.t > create mode 100755 tools/stockrotation.pl > >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 964359ecf8..721c0d66d6 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -31,6 +31,8 @@ use Koha::Item::Transfer::Limits; > use Koha::Item::Transfers; > use Koha::Patrons; > use Koha::Libraries; >+use Koha::StockRotationItem; >+use Koha::StockRotationRotas; > > use base qw(Koha::Object); > >@@ -282,7 +284,52 @@ sub current_holds { > return Koha::Holds->_new_from_dbic($hold_rs); > } > >-=head3 type >+=head3 stockrotationitem >+ >+ my $sritem = Koha::Item->stockrotationitem; >+ >+Returns the stock rotation item associated with the current item. >+ >+=cut >+ >+sub stockrotationitem { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationitem; >+ return 0 if !$rs; >+ return Koha::StockRotationItem->_new_from_dbic( $rs ); >+} >+ >+=head3 add_to_rota >+ >+ my $item = $item->add_to_rota($rota_id); >+ >+Add this item to the rota identified by $ROTA_ID, which means associating it >+with the first stage of that rota. Should this item already be associated >+with a rota, then we will move it to the new rota. >+ >+=cut >+ >+sub add_to_rota { >+ my ( $self, $rota_id ) = @_; >+ Koha::StockRotationRotas->find($rota_id)->add_item($self->itemnumber); >+ return $self; >+} >+ >+=head3 biblio >+ >+ my $biblio = $item->biblio; >+ >+Returns the biblio associated with the current item. >+ >+=cut >+ >+sub biblio { >+ my ( $self ) = @_; >+ my $rs = $self->_result->biblio; >+ return Koha::Biblio->_new_from_dbic( $rs ); >+} >+ >+=head3 _type > > =cut > >diff --git a/Koha/Library.pm b/Koha/Library.pm >index 9e6d616036..7877174ec5 100644 >--- a/Koha/Library.pm >+++ b/Koha/Library.pm >@@ -24,6 +24,7 @@ use Carp; > use C4::Context; > > use Koha::Database; >+use Koha::StockRotationStages; > > use base qw(Koha::Object); > >@@ -41,6 +42,51 @@ TODO: Ask the author to add a proper description > > =cut > >+sub get_categories { >+ my ( $self, $params ) = @_; >+ # TODO This should return Koha::LibraryCategories >+ return $self->{_result}->categorycodes( $params ); >+} >+ >+=head3 update_categories >+ >+TODO: Ask the author to add a proper description >+ >+=cut >+ >+sub update_categories { >+ my ( $self, $categories ) = @_; >+ $self->_result->delete_related( 'branchrelations' ); >+ $self->add_to_categories( $categories ); >+} >+ >+=head3 add_to_categories >+ >+TODO: Ask the author to add a proper description >+ >+=cut >+ >+sub add_to_categories { >+ my ( $self, $categories ) = @_; >+ for my $category ( @$categories ) { >+ $self->_result->add_to_categorycodes( $category->_result ); >+ } >+} >+ >+=head3 stockrotationstages >+ >+ my $stages = Koha::Library->stockrotationstages; >+ >+Returns the stockrotation stages associated with this Library. >+ >+=cut >+ >+sub stockrotationstages { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationstages; >+ return Koha::StockRotationStages->_new_from_dbic( $rs ); >+} >+ > =head3 get_effective_marcorgcode > > my $marcorgcode = Koha::Libraries->find( $library_id )->get_effective_marcorgcode(); >diff --git a/Koha/REST/V1/Stage.pm b/Koha/REST/V1/Stage.pm >new file mode 100644 >index 0000000000..485384a41b >--- /dev/null >+++ b/Koha/REST/V1/Stage.pm >@@ -0,0 +1,60 @@ >+package Koha::REST::V1::Stage; >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Mojo::Base 'Mojolicious::Controller'; >+ >+use Koha::StockRotationRotas; >+use Koha::StockRotationStages; >+ >+=head1 NAME >+ >+Koha::REST::V1::Stage >+ >+=head2 Operations >+ >+=head3 move >+ >+Move a stage up or down the stockrotation rota. >+ >+=cut >+ >+sub move { >+ my $c = shift->openapi->valid_input or return; >+ my $input = $c->validation->output; >+ >+ my $rota = Koha::StockRotationRotas->find( $input->{rota_id} ); >+ my $stage = Koha::StockRotationStages->find( $input->{stage_id} ); >+ >+ if ( $stage && $rota ) { >+ my $result = $stage->move_to( $input->{position} ); >+ return $c->render( openapi => {}, status => 200 ) if $result; >+ return $c->render( >+ openapi => { error => "Bad request - new position invalid" }, >+ status => 400 >+ ); >+ } >+ else { >+ return $c->render( >+ openapi => { error => "Not found - Invalid rota or stage ID" }, >+ status => 404 >+ ); >+ } >+} >+ >+1; >diff --git a/Koha/StockRotationItem.pm b/Koha/StockRotationItem.pm >new file mode 100644 >index 0000000000..e451bb08db >--- /dev/null >+++ b/Koha/StockRotationItem.pm >@@ -0,0 +1,273 @@ >+package Koha::StockRotationItem; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use DateTime; >+use DateTime::Duration; >+use Koha::Database; >+use Koha::DateUtils qw/dt_from_string/; >+use Koha::Item::Transfer; >+use Koha::Item; >+use Koha::StockRotationStage; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationItem - Koha StockRotationItem Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationItem class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationitem'; >+} >+ >+=head3 itemnumber >+ >+ my $item = Koha::StockRotationItem->itemnumber; >+ >+Returns the item associated with the current stock rotation item. >+ >+=cut >+ >+sub itemnumber { >+ my ( $self ) = @_; >+ my $rs = $self->_result->itemnumber; >+ return Koha::Item->_new_from_dbic( $rs ); >+} >+ >+=head3 stage >+ >+ my $stage = Koha::StockRotationItem->stage; >+ >+Returns the stage associated with the current stock rotation item. >+ >+=cut >+ >+sub stage { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stage; >+ return Koha::StockRotationStage->_new_from_dbic( $rs ); >+} >+ >+=head3 needs_repatriating >+ >+ 1|0 = $item->needs_repatriating; >+ >+Return 1 if this item is currently not at the library it should be at >+according to our stockrotation plan. >+ >+=cut >+ >+sub needs_repatriating { >+ my ( $self ) = @_; >+ my ( $item, $stage ) = ( $self->itemnumber, $self->stage ); >+ if ( $self->itemnumber->get_transfer ) { >+ return 0; # We're in transit. >+ } elsif ( $item->holdingbranch ne $stage->branchcode_id >+ || $item->homebranch ne $stage->branchcode_id ) { >+ return 1; # We're not where we should be. >+ } else { >+ return 0; # We're at home. >+ } >+} >+ >+=head3 needs_advancing >+ >+ 1|0 = $item->needs_advancing; >+ >+Return 1 if this item is ready to be moved on to the next stage in its rota. >+ >+=cut >+ >+sub needs_advancing { >+ my ( $self ) = @_; >+ return 0 if $self->itemnumber->get_transfer; # intransfer: don't advance. >+ return 1 if $self->fresh; # Just on rota: advance. >+ my $completed = $self->itemnumber->_result->branchtransfers->search( >+ { 'comments' => "StockrotationAdvance" }, >+ { order_by => { -desc => 'datearrived' } } >+ ); >+ # Do maths on whether we need to be moved on. >+ if ( $completed->count ) { >+ my $arrival = dt_from_string( >+ $completed->next->datearrived, 'iso' >+ ); >+ my $duration = DateTime::Duration >+ ->new( days => $self->stage->duration ); >+ if ( $arrival + $duration le DateTime->now ) { >+ return 1; >+ } else { >+ return 0; >+ } >+ } else { >+ die "We have no historical branch transfer; this should not have happened!"; >+ } >+} >+ >+=head3 repatriate >+ >+ 1|0 = $sritem->repatriate >+ >+Put this item into branch transfer with 'StockrotationCorrection' comment, so >+that it may return to it's stage.branch to continue its rota as normal. >+ >+=cut >+ >+sub repatriate { >+ my ( $self, $msg ) = @_; >+ # Create the transfer. >+ my $transfer_stored = Koha::Item::Transfer->new({ >+ 'itemnumber' => $self->itemnumber_id, >+ 'frombranch' => $self->itemnumber->holdingbranch, >+ 'tobranch' => $self->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'comments' => $msg || "StockrotationRepatriation", >+ })->store; >+ $self->itemnumber->homebranch($self->stage->branchcode_id)->store; >+ return $transfer_stored; >+} >+ >+=head3 advance >+ >+ 1|0 = $sritem->advance; >+ >+Put this item into branch transfer with 'StockrotationAdvance' comment, to >+transfer it to the next stage in its rota. >+ >+If this is the last stage in the rota and this rota is cyclical, we return to >+the first stage. If it is not cyclical, then we delete this >+StockRotationItem. >+ >+If this item is 'indemand', and advance is invoked, we disable 'indemand' and >+advance the item as per usual. >+ >+=cut >+ >+sub advance { >+ my ( $self ) = @_; >+ my $item = $self->itemnumber; >+ my $transfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $self->itemnumber_id, >+ 'frombranch' => $item->holdingbranch, >+ 'datesent' => DateTime->now, >+ 'comments' => "StockrotationAdvance" >+ }); >+ >+ if ( $self->indemand && !$self->fresh ) { >+ $self->indemand(0)->store; # De-activate indemand >+ $transfer->tobranch($self->stage->branchcode_id); >+ $transfer->datearrived(DateTime->now); >+ } else { >+ # Find and update our stage. >+ my $stage = $self->stage; >+ my $new_stage; >+ if ( $self->fresh ) { # Just added to rota >+ $new_stage = $self->stage->first_sibling || $self->stage; >+ $transfer->tobranch($new_stage->branchcode_id); >+ $transfer->datearrived(DateTime->now) # Already at first branch >+ if $item->holdingbranch eq $new_stage->branchcode_id; >+ $self->fresh(0)->store; # Reset fresh >+ } elsif ( !$stage->last_sibling ) { # Last stage >+ if ( $stage->rota->cyclical ) { # Cyclical rota? >+ # Revert to first stage. >+ $new_stage = $stage->first_sibling || $stage; >+ $transfer->tobranch($new_stage->branchcode_id); >+ $transfer->datearrived(DateTime->now); >+ } else { >+ $self->delete; # StockRotationItem is done. >+ return 1; >+ } >+ } else { >+ # Just advance. >+ $new_stage = $self->stage->next_sibling; >+ } >+ $self->stage_id($new_stage->stage_id)->store; # Set new stage >+ $item->homebranch($new_stage->branchcode_id)->store; # Update homebranch >+ $transfer->tobranch($new_stage->branchcode_id); # Send to new branch >+ } >+ >+ return $transfer->store; >+} >+ >+=head3 investigate >+ >+ my $report = $item->investigate; >+ >+Return the base set of information, namely this individual item's report, for >+generating stockrotation reports about this stockrotationitem. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ my $item_report = { >+ title => $self->itemnumber->_result->biblioitem >+ ->biblionumber->title, >+ author => $self->itemnumber->_result->biblioitem >+ ->biblionumber->author, >+ callnumber => $self->itemnumber->itemcallnumber, >+ location => $self->itemnumber->location, >+ onloan => $self->itemnumber->onloan, >+ barcode => $self->itemnumber->barcode, >+ itemnumber => $self->itemnumber_id, >+ branch => $self->itemnumber->_result->holdingbranch, >+ object => $self, >+ }; >+ my $reason; >+ if ( $self->fresh ) { >+ $reason = 'initiation'; >+ } elsif ( $self->needs_repatriating ) { >+ $reason = 'repatriation'; >+ } elsif ( $self->needs_advancing ) { >+ $reason = 'advancement'; >+ $reason = 'in-demand' if $self->indemand; >+ } else { >+ $reason = 'not-ready'; >+ } >+ $item_report->{reason} = $reason; >+ >+ return $item_report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationItems.pm b/Koha/StockRotationItems.pm >new file mode 100644 >index 0000000000..c78c2676c3 >--- /dev/null >+++ b/Koha/StockRotationItems.pm >@@ -0,0 +1,128 @@ >+package Koha::StockRotationItems; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationItem; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationItems - Koha StockRotationItems Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationItems class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationitem'; >+} >+ >+=head3 object_class >+ >+=cut >+ >+sub object_class { >+ return 'Koha::StockRotationItem'; >+} >+ >+=head3 investigate >+ >+ my $report = $items->investigate; >+ >+Return a stockrotation report about this set of stockrotationitems. >+ >+In this part of the overall investigation process we split individual item >+reports into appropriate action segments of our items report and increment >+some counters. >+ >+The report generated here will be used on the stage level to slot our item >+reports into appropriate sections of the branched report. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ >+ my $items_report = { >+ items => [], >+ log => [], >+ initiable_items => [], >+ repatriable_items => [], >+ advanceable_items => [], >+ indemand_items => [], >+ actionable => 0, >+ stationary => 0, >+ }; >+ while ( my $item = $self->next ) { >+ my $report = $item->investigate; >+ if ( $report->{reason} eq 'initiation' ) { >+ $items_report->{initiable}++; >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{initiable_items}}, $report; >+ } elsif ( $report->{reason} eq 'repatriation' ) { >+ $items_report->{repatriable}++; >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{repatriable_items}}, $report; >+ } elsif ( $report->{reason} eq 'advancement' ) { >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{advanceable_items}}, $report; >+ } elsif ( $report->{reason} eq 'in-demand' ) { >+ $items_report->{actionable}++; >+ push @{$items_report->{items}}, $report; >+ push @{$items_report->{indemand_items}}, $report; >+ } else { >+ $items_report->{stationary}++; >+ push @{$items_report->{log}}, $report; >+ } >+ } >+ >+ return $items_report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationRota.pm b/Koha/StockRotationRota.pm >new file mode 100644 >index 0000000000..fdf52fccd9 >--- /dev/null >+++ b/Koha/StockRotationRota.pm >@@ -0,0 +1,182 @@ >+package Koha::StockRotationRota; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationStages; >+use Koha::StockRotationItem; >+use Koha::StockRotationItems; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationRota - Koha StockRotationRota Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationRota class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 stockrotationstages >+ >+ my $stages = Koha::StockRotationRota->stockrotationstages; >+ >+Returns the stages associated with the current rota. >+ >+=cut >+ >+sub stockrotationstages { >+ my ( $self ) = @_; >+ my $rs = $self->_result->stockrotationstages; >+ return Koha::StockRotationStages->_new_from_dbic( $rs ); >+} >+ >+=head3 add_item >+ >+ my $rota = $rota->add_item($itemnumber); >+ >+Add item identified by $ITEMNUMBER to this rota, which means we associate it >+with the first stage of this rota. Should the item already be associated with >+a rota, move it from that rota to this rota. >+ >+=cut >+ >+sub add_item { >+ my ( $self, $itemnumber ) = @_; >+ my $sritem = Koha::StockRotationItems->find($itemnumber); >+ if ($sritem) { >+ $sritem->stage_id($self->first_stage->stage_id) >+ ->indemand(0)->fresh(1)->store; >+ } else { >+ $sritem = Koha::StockRotationItem->new({ >+ itemnumber_id => $itemnumber, >+ stage_id => $self->first_stage->stage_id, >+ indemand => 0, >+ fresh => 1, >+ })->store; >+ } >+ return $self; >+} >+ >+=head3 first_stage >+ >+ my $stage = $rota->first_stage; >+ >+Return the first stage attached to this rota (the one that has an undefined >+`stagebefore`). >+ >+=cut >+ >+sub first_stage { >+ my ( $self ) = @_; >+ my $guess = $self->stockrotationstages->next; >+ my $stage = $guess->first_sibling; >+ return ( $stage ) ? $stage : $guess; >+} >+ >+=head3 stockrotationitems >+ >+ my $items = $rota->stockrotationitems; >+ >+Return all items associated with this rota via its stages. >+ >+=cut >+ >+sub stockrotationitems { >+ my ( $self ) = @_; >+ my $rs = Koha::StockRotationItems->search( >+ { 'stage.rota_id' => $self->rota_id }, { join => [ qw/stage/ ] } >+ ); >+ return $rs; >+} >+ >+=head3 investigate >+ >+ my $report = $rota->investigate($report_so_far); >+ >+Aim here is to return $report augmented with content for this rota. We >+delegate to $stage->investigate. >+ >+The report will include some basic information and 2 primary reports: >+ >+- per rota report in 'rotas'. This report is mainly used by admins to do check >+ & compare results. >+ >+- branched report in 'branched'. This is the workhorse: emails to libraries >+ are compiled from these reports, and they will have the actionable work. >+ >+Both reports are generated in stage based investigations; the rota report is >+then glued into place at this stage. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ my $count = $self->stockrotationitems->count; >+ $report->{sum_items} += $count; >+ >+ if ( $self->active ) { >+ $report->{rotas_active}++; >+ # stockrotationstages->investigate augments $report with the stage's >+ # content. This is how 'branched' slowly accumulates all items. >+ $report = $self->stockrotationstages->investigate($report); >+ # Add our rota report to the full report. >+ push @{$report->{rotas}}, { >+ name => $self->title, >+ id => $self->rota_id, >+ items => $report->{tmp_items} || [], >+ log => $report->{tmp_log} || [], >+ }; >+ delete $report->{tmp_items}; >+ delete $report->{tmp_log}; >+ } else { # Rota is not active. >+ $report->{rotas_inactive}++; >+ $report->{items_inactive} += $count; >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationrota'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationRotas.pm b/Koha/StockRotationRotas.pm >new file mode 100644 >index 0000000000..ba905ff2ed >--- /dev/null >+++ b/Koha/StockRotationRotas.pm >@@ -0,0 +1,105 @@ >+package Koha::StockRotationRotas; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationRota; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationRotas - Koha StockRotationRotas Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationRotas class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 investigate >+ >+ my $report = $rotas->investigate; >+ >+Return a report detailing the current status and required actions for all >+relevant items spread over rotas. >+ >+See Koha::StockRotationRota->investigate for details. >+ >+=cut >+ >+sub investigate { >+ my ( $self ) = @_; >+ >+ my $report = { >+ actionable => 0, >+ advanceable => 0, >+ initiable => 0, >+ items_inactive => 0, >+ repatriable => 0, >+ rotas_active => 0, >+ rotas_inactive => 0, >+ stationary => 0, >+ sum_items => 0, >+ sum_rotas => $self->count, >+ branched => {}, >+ rotas => [], >+ items => [], >+ }; >+ >+ while ( my $rota = $self->next ) { >+ $report = $rota->investigate($report) >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationrota'; >+} >+ >+=head3 object_class >+ >+=cut >+ >+sub object_class { >+ return 'Koha::StockRotationRota'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationStage.pm b/Koha/StockRotationStage.pm >new file mode 100644 >index 0000000000..dc76ba83f6 >--- /dev/null >+++ b/Koha/StockRotationStage.pm >@@ -0,0 +1,419 @@ >+package Koha::StockRotationStage; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::Library; >+use Koha::StockRotationRota; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+StockRotationStage - Koha StockRotationStage Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationStage class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationstage'; >+} >+ >+sub _relation { >+ my ( $self, $method, $type ) = @_; >+ return sub { >+ my $rs = $self->_result->$method; >+ return 0 if !$rs; >+ my $namespace = 'Koha::' . $type; >+ return $namespace->_new_from_dbic( $rs ); >+ } >+} >+ >+=head3 stockrotationitems >+ >+ my $stages = Koha::StockRotationStage->stockrotationitems; >+ >+Returns the items associated with the current stage. >+ >+=cut >+ >+sub stockrotationitems { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ stockrotationitems StockRotationItems /)}; >+} >+ >+=head3 branchcode >+ >+ my $branch = Koha::StockRotationStage->branchcode; >+ >+Returns the branch associated with the current stage. >+ >+=cut >+ >+sub branchcode { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ branchcode Library /)}; >+} >+ >+=head3 rota >+ >+ my $rota = Koha::StockRotationStage->rota; >+ >+Returns the rota associated with the current stage. >+ >+=cut >+ >+sub rota { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ rota StockRotationRota /)}; >+} >+ >+=head3 siblings >+ >+ my $siblings = $stage->siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ siblings StockRotationStages /)}; >+} >+ >+=head3 next_siblings >+ >+ my $next_siblings = $stage->next_siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub next_siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ next_siblings StockRotationStages /)}; >+} >+ >+=head3 previous_siblings >+ >+ my $previous_siblings = $stage->previous_siblings; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub previous_siblings { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ previous_siblings StockRotationStages /)}; >+} >+ >+=head3 next_sibling >+ >+ my $next = $stage->next_sibling; >+ >+Koha::Object wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub next_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ next_sibling StockRotationStage /)}; >+} >+ >+=head3 previous_sibling >+ >+ my $previous = $stage->previous_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub previous_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ previous_sibling StockRotationStage /)}; >+} >+ >+=head3 first_sibling >+ >+ my $first = $stage->first_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub first_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ first_sibling StockRotationStage /)}; >+} >+ >+=head3 last_sibling >+ >+ my $last = $stage->last_sibling; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub last_sibling { >+ my ( $self ) = @_; >+ return &{$self->_relation(qw/ last_sibling StockRotationStage /)}; >+} >+ >+=head3 move_previous >+ >+ 1|0 = $stage->move_previous; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_previous { >+ my ( $self ) = @_; >+ return $self->_result->move_previous; >+} >+ >+=head3 move_next >+ >+ 1|0 = $stage->move_next; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_next { >+ my ( $self ) = @_; >+ return $self->_result->move_next; >+} >+ >+=head3 move_first >+ >+ 1|0 = $stage->move_first; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_first { >+ my ( $self ) = @_; >+ return $self->_result->move_first; >+} >+ >+=head3 move_last >+ >+ 1|0 = $stage->move_last; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_last { >+ my ( $self ) = @_; >+ return $self->_result->move_last; >+} >+ >+=head3 move_to >+ >+ 1|0 = $stage->move_to($position); >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_to { >+ my ( $self, $position ) = @_; >+ return $self->_result->move_to($position) >+ if ( $position le $self->rota->stockrotationstages->count ); >+ return 0; >+} >+ >+=head3 move_to_group >+ >+ 1|0 = $stage->move_to_group($rota_id, [$position]); >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub move_to_group { >+ my ( $self, $rota_id, $position ) = @_; >+ return $self->_result->move_to_group($rota_id, $position); >+} >+ >+=head3 delete >+ >+ 1|0 = $stage->delete; >+ >+Koha::Object Wrapper around DBIx::Class::Ordered. >+ >+=cut >+ >+sub delete { >+ my ( $self ) = @_; >+ return $self->_result->delete; >+} >+ >+=head3 investigate >+ >+ my $report = $stage->investigate($report_so_far); >+ >+Return a stage based report. This report will mutate and augment the report >+that is passed to it. It slots item reports into the branched and temporary >+rota sections of the report. It also increments a number of counters. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ my $new_stage = $self->next_sibling; >+ my $duration = $self->duration; >+ # Generate stage items report >+ my $items_report = $self->stockrotationitems->investigate; >+ >+ # Merge into general report >+ >+ ## Branched indexes >+ ### The branched indexes work as follows: >+ ### - They contain information about the relevant branch >+ ### - They contain an index of actionable items for that branch >+ ### - They contain an index of non-actionable items for that branch >+ >+ ### Items are assigned to a particular branched index as follows: >+ ### - 'advanceable' : assigned to branch of the current stage >+ ### (this should also be the current holding branch) >+ ### - 'log' items are always assigned to branch of current stage. >+ ### - 'indemand' : assigned to branch of current stage >+ ### (this should also be the current holding branch) >+ ### - 'initiable' : assigned to the current holding branch of item >+ ### - 'repatriable' : assigned to the current holding branch of item >+ >+ ### 'Advanceable', 'log', 'indemand': >+ >+ # Set up our stage branch info. >+ my $stagebranch = $self->_result->branchcode; >+ my $stagebranchcode = $stagebranch->branchcode; >+ >+ # Initiate our stage branch index if it does not yet exist. >+ if ( !$report->{branched}->{$stagebranchcode} ) { >+ $report->{branched}->{$stagebranchcode} = { >+ code => $stagebranchcode, >+ name => $stagebranch->branchname, >+ email => $stagebranch->branchreplyto >+ ? $stagebranch->branchreplyto >+ : $stagebranch->branchemail, >+ phone => $stagebranch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ >+ push @{$report->{branched}->{$stagebranchcode}->{items}}, >+ @{$items_report->{advanceable_items}}; >+ push @{$report->{branched}->{$stagebranchcode}->{log}}, >+ @{$items_report->{log}}; >+ push @{$report->{branched}->{$stagebranchcode}->{items}}, >+ @{$items_report->{indemand_items}}; >+ >+ ### 'Initiable' & 'Repatriable' >+ foreach my $ireport (@{$items_report->{initiable_items}}) { >+ my $branch = $ireport->{branch}; >+ my $branchcode = $branch->branchcode; >+ if ( !$report->{branched}->{$branchcode} ) { >+ $report->{branched}->{$branchcode} = { >+ code => $branchcode, >+ name => $branch->branchname, >+ email => $stagebranch->branchreplyto >+ ? $stagebranch->branchreplyto >+ : $stagebranch->branchemail, >+ phone => $branch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ push @{$report->{branched}->{$branchcode}->{items}}, $ireport; >+ } >+ >+ foreach my $ireport (@{$items_report->{repatriable_items}}) { >+ my $branch = $ireport->{branch}; >+ my $branchcode = $branch->branchcode; >+ if ( !$report->{branched}->{$branchcode} ) { >+ $report->{branched}->{$branchcode} = { >+ code => $branchcode, >+ name => $branch->branchname, >+ email => $stagebranch->branchreplyto >+ ? $stagebranch->branchreplyto >+ : $stagebranch->branchemail, >+ phone => $branch->branchphone, >+ items => [], >+ log => [], >+ }; >+ } >+ push @{$report->{branched}->{$branchcode}->{items}}, $ireport; >+ } >+ >+ ## Per rota indexes >+ ### Per rota indexes are item reports pushed into the index for the >+ ### current rota. We don't know where that index is yet as we don't know >+ ### about the current rota. To resolve this we assign our items and log >+ ### to tmp indexes. They will be merged into the proper rota index at the >+ ### rota level. >+ push @{$report->{tmp_items}}, @{$items_report->{items}}; >+ push @{$report->{tmp_log}}, @{$items_report->{log}}; >+ >+ ## Collection of items >+ ### Finally we just add our collection of items to the full item index. >+ push @{$report->{items}}, @{$items_report->{items}}; >+ >+ ## Assemble counters >+ $report->{actionable} += $items_report->{actionable}; >+ $report->{indemand} += scalar @{$items_report->{indemand_items}}; >+ $report->{advanceable} += scalar @{$items_report->{advanceable_items}}; >+ $report->{initiable} += scalar @{$items_report->{initiable_items}}; >+ $report->{repatriable} += scalar @{$items_report->{repatriable_items}}; >+ $report->{stationary} += scalar @{$items_report->{log}}; >+ >+ return $report; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/StockRotationStages.pm b/Koha/StockRotationStages.pm >new file mode 100644 >index 0000000000..1b8599926b >--- /dev/null >+++ b/Koha/StockRotationStages.pm >@@ -0,0 +1,90 @@ >+package Koha::StockRotationStages; >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use Koha::StockRotationStage; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+StockRotationStages - Koha StockRotationStages Object class >+ >+=head1 SYNOPSIS >+ >+StockRotationStages class used primarily by stockrotation .pls and the stock >+rotation cron script. >+ >+=head1 DESCRIPTION >+ >+Standard Koha::Objects definitions, and additional methods. >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 investigate >+ >+ my $report = $stages->investigate($rota_so_far); >+ >+Return a report detailing the current status and required actions for all >+relevant items spread over the set of stages. >+ >+For details of intent and context of this procedure, please see >+Koha::StockRotationRota->investigate. >+ >+=cut >+ >+sub investigate { >+ my ( $self, $report ) = @_; >+ >+ while ( my $stage = $self->next ) { >+ $report = $stage->investigate($report); >+ } >+ >+ return $report; >+} >+ >+=head3 _type >+ >+=cut >+ >+sub _type { >+ return 'Stockrotationstage'; >+} >+ >+=head3 object_class >+ >+=cut >+ >+sub object_class { >+ return 'Koha::StockRotationStage'; >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/Koha/Util/StockRotation.pm b/Koha/Util/StockRotation.pm >new file mode 100644 >index 0000000000..13c78e5675 >--- /dev/null >+++ b/Koha/Util/StockRotation.pm >@@ -0,0 +1,247 @@ >+package Koha::Util::StockRotation; >+ >+# Module contains subroutines used with Stock Rotation >+# >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Items; >+use Koha::StockRotationItems; >+use Koha::Database; >+ >+our ( @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS ); >+BEGIN { >+ require Exporter; >+ @ISA = qw( Exporter ); >+ @EXPORT = qw( ); >+ @EXPORT_OK = qw( >+ get_branches >+ get_stages >+ toggle_indemand >+ remove_from_stage >+ get_barcodes_status >+ add_items_to_rota >+ move_to_next_stage >+ ); >+ %EXPORT_TAGS = ( ALL => [ @EXPORT_OK, @EXPORT ] ); >+} >+ >+=head1 NAME >+ >+Koha::Util::StockRotation - utility class with routines for Stock Rotation >+ >+=head1 FUNCTIONS >+ >+=head2 get_branches >+ >+ returns all branches ordered by branchname as an array, each element >+ contains a hashref containing branch details >+ >+=cut >+ >+sub get_branches { >+ >+ return Koha::Libraries->search( >+ {}, >+ { order_by => ['branchname'] } >+ )->unblessed; >+ >+} >+ >+=head2 get_stages >+ >+ returns an arrayref of StockRotationStage objects representing >+ all stages for a passed rota >+ >+=cut >+ >+sub get_stages { >+ >+ my $rota = shift; >+ >+ my @out = (); >+ >+ if ($rota->stockrotationstages->count > 0) { >+ >+ push @out, $rota->first_stage->unblessed; >+ >+ push @out, @{$rota->first_stage->siblings->unblessed}; >+ >+ } >+ >+ return \@out; >+} >+ >+=head2 toggle_indemand >+ >+ given an item's ID & stage ID toggle that item's in_demand >+ status on that stage >+ >+=cut >+ >+sub toggle_indemand { >+ >+ my ($item_id, $stage_id) = @_; >+ >+ # Get the item object >+ my $item = Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ ); >+ >+ # Toggle the item's indemand flag >+ my $new_indemand = ($item->indemand == 1) ? 0 : 1; >+ >+ $item->indemand($new_indemand)->store; >+ >+} >+ >+=head2 move_to_next_stage >+ >+ given an item's ID and stage ID, move it >+ to the next stage on the rota >+ >+=cut >+ >+sub move_to_next_stage { >+ >+ my ($item_id, $stage_id) = shift; >+ >+ # Get the item object >+ my $item = Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ ); >+ >+ $item->advance; >+ >+} >+ >+=head2 remove_from_stage >+ >+ given an item's ID & stage ID, remove that item from that stage >+ >+=cut >+ >+sub remove_from_stage { >+ >+ my ($item_id, $stage_id) = @_; >+ >+ # Get the item object and delete it >+ Koha::StockRotationItems->find( >+ { >+ itemnumber_id => $item_id, >+ stage_id => $stage_id >+ } >+ )->delete; >+ >+} >+ >+=head2 get_barcodes_status >+ >+ take an arrayref of barcodes and a status hashref and populate it >+ >+=cut >+ >+sub get_barcodes_status { >+ >+ my ($rota_id, $barcodes, $status) = @_; >+ >+ # Get the items associated with these barcodes >+ my $items = Koha::Items->search( >+ { >+ barcode => { '-in' => $barcodes } >+ }, >+ { >+ prefetch => 'stockrotationitem' >+ } >+ ); >+ # Get an array of barcodes that were found >+ # Assign each barcode's status >+ my @found = (); >+ while (my $item = $items->next) { >+ >+ push @found, $item->barcode if $item->barcode; >+ >+ # Check if it's on a rota >+ my $on_rota = $item->stockrotationitem; >+ >+ # It is on a rota >+ if ($on_rota) { >+ >+ # Check if it's on this rota >+ if ($on_rota->stage->rota->rota_id == $rota_id) { >+ >+ # It's on this rota >+ push @{$status->{on_this}}, $item; >+ >+ } else { >+ >+ # It's on another rota >+ push @{$status->{on_other}}, $item; >+ >+ } >+ >+ } else { >+ >+ # Item is not on a rota >+ push @{$status->{ok}}, $item; >+ >+ } >+ >+ } >+ >+ # Create an array of barcodes supplied in the file that >+ # were not found in the catalogue >+ my %found_in_cat = map{ $_ => 1 } @found; >+ push @{$status->{not_found}}, grep( >+ !defined $found_in_cat{$_}, @{$barcodes} >+ ); >+ >+} >+ >+=head2 add_items_to_rota >+ >+ take an arrayref of Koha::Item objects and add them to the passed rota >+ >+=cut >+ >+sub add_items_to_rota { >+ >+ my ($rota_id, $items) = @_; >+ >+ foreach my $item(@{$items}) { >+ >+ $item->add_to_rota($rota_id); >+ >+ } >+ >+} >+ >+1; >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json >index c9d8e881b5..c8870db2a1 100644 >--- a/api/v1/swagger/paths.json >+++ b/api/v1/swagger/paths.json >@@ -34,5 +34,8 @@ > }, > "/illrequests": { > "$ref": "paths/illrequests.json#/~1illrequests" >+ }, >+ "/rotas/{rota_id}/stages/{stage_id}/position": { >+ "$ref": "paths/rotas.json#/~1rotas~1{rota_id}~1stages~1{stage_id}~1position" > } > } >diff --git a/api/v1/swagger/paths/rotas.json b/api/v1/swagger/paths/rotas.json >new file mode 100644 >index 0000000000..0cdda30317 >--- /dev/null >+++ b/api/v1/swagger/paths/rotas.json >@@ -0,0 +1,79 @@ >+{ >+ "/rotas/{rota_id}/stages/{stage_id}/position": { >+ "put": { >+ "x-mojo-to": "Stage#move", >+ "operationId": "moveStage", >+ "tags": ["rotas"], >+ "parameters": [{ >+ "name": "rota_id", >+ "in": "path", >+ "required": true, >+ "description": "A rotas ID", >+ "type": "integer" >+ }, { >+ "name": "stage_id", >+ "in": "path", >+ "required": true, >+ "description": "A stages ID", >+ "type": "integer" >+ }, { >+ "name": "position", >+ "in": "body", >+ "required": true, >+ "description": "A stages position in the rota", >+ "schema": { >+ "type": "integer" >+ } >+ }], >+ "produces": [ >+ "application/json" >+ ], >+ "responses": { >+ "200": { >+ "description": "OK" >+ }, >+ "400": { >+ "description": "Bad request", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "401": { >+ "description": "Authentication required", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "404": { >+ "description": "Position not found", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "500": { >+ "description": "Internal server error", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "503": { >+ "description": "Under maintenance", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "permissions": { >+ "borrowers": "1" >+ } >+ } >+ } >+ } >+} >diff --git a/catalogue/stockrotation.pl b/catalogue/stockrotation.pl >new file mode 100755 >index 0000000000..8b6a32451f >--- /dev/null >+++ b/catalogue/stockrotation.pl >@@ -0,0 +1,179 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 stockrotation.pl >+ >+ Script to manage item assignments to stock rotation rotas. Including their >+ assiciated stages >+ >+=cut >+ >+use Modern::Perl; >+use CGI; >+ >+use C4::Auth; >+use C4::Output; >+use C4::Search; >+ >+use Koha::Biblio; >+use Koha::Item; >+use Koha::StockRotationRotas; >+use Koha::StockRotationStages; >+use Koha::Util::StockRotation qw(:ALL); >+ >+my $input = new CGI; >+ >+unless (C4::Context->preference('StockRotation')) { >+ # redirect to Intranet home if self-check is not enabled >+ print $input->redirect("/cgi-bin/koha/mainpage.pl"); >+ exit; >+} >+ >+my %params = $input->Vars(); >+ >+my $op = $params{op}; >+ >+my $biblionumber = $input->param('biblionumber'); >+ >+my ($template, $loggedinuser, $cookie) = get_template_and_user( >+ { >+ template_name => 'catalogue/stockrotation.tt', >+ query => $input, >+ type => 'intranet', >+ authnotrequired => 0, >+ flagsrequired => { >+ catalogue => 1, >+ stockrotation => 'manage_rota_items', >+ }, >+ } >+); >+ >+if (!defined $op) { >+ >+ # List all items along with their associated rotas >+ my $biblio = Koha::Biblios->find($biblionumber); >+ >+ my $items = $biblio->items; >+ >+ # Get only rotas with stages >+ my $rotas = Koha::StockRotationRotas->search( >+ { >+ 'stockrotationstages.stage_id' => { '!=', undef } >+ }, >+ { >+ join => 'stockrotationstages', >+ collapse => 1, >+ order_by => 'title' >+ } >+ ); >+ >+ # Construct a model to pass to the view >+ my @item_data = (); >+ >+ while (my $item = $items->next) { >+ >+ my $item_hashref = { >+ bib_item => $item >+ }; >+ >+ my $stockrotationitem = $item->stockrotationitem; >+ >+ # If this item is on a rota >+ if ($stockrotationitem != 0) { >+ >+ # This item's rota >+ my $rota = $stockrotationitem->stage->rota; >+ >+ # This rota's stages >+ my $stages = get_stages($rota); >+ >+ $item_hashref->{rota} = $rota; >+ >+ $item_hashref->{stockrotationitem} = $stockrotationitem; >+ >+ $item_hashref->{stages} = $stages; >+ >+ } >+ >+ push @item_data, $item_hashref; >+ >+ } >+ >+ $template->param( >+ no_op_set => 1, >+ rotas => $rotas, >+ items => \@item_data, >+ branches => get_branches(), >+ biblio => $biblio, >+ biblionumber => $biblio->biblionumber, >+ stockrotationview => 1, >+ C4::Search::enabled_staff_search_views >+ ); >+ >+} elsif ($op eq "toggle_in_demand") { >+ >+ # Toggle in demand >+ toggle_indemand($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=$biblionumber"); >+ >+} elsif ($op eq "remove_item_from_stage") { >+ >+ # Remove from the stage >+ remove_from_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=$biblionumber"); >+ >+} elsif ($op eq "move_to_next_stage") { >+ >+ move_to_next_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to items list >+ print $input->redirect("?biblionumber=" . $params{biblionumber}); >+ >+} elsif ($op eq "add_item_to_rota") { >+ >+ my $item = Koha::Items->find($params{item_id}); >+ >+ $item->add_to_rota($params{rota_id}); >+ >+ print $input->redirect("?biblionumber=" . $params{biblionumber}); >+ >+} elsif ($op eq "confirm_remove_from_rota") { >+ >+ $template->param( >+ op => $params{op}, >+ stage_id => $params{stage_id}, >+ item_id => $params{item_id}, >+ biblionumber => $params{biblionumber}, >+ stockrotationview => 1, >+ C4::Search::enabled_staff_search_views >+ ); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >diff --git a/installer/data/mysql/en/mandatory/sample_notices.sql b/installer/data/mysql/en/mandatory/sample_notices.sql >index c36458ec26..1891c1f231 100644 >--- a/installer/data/mysql/en/mandatory/sample_notices.sql >+++ b/installer/data/mysql/en/mandatory/sample_notices.sql >@@ -176,8 +176,9 @@ INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title` > ('circulation', 'AR_SLIP', '', 'Article request - print slip', 0, 'Article request', 'Article request:\r\n\r\n<<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nTitle: <<biblio.title>>\r\nBarcode: <<items.barcode>>\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n', 'print'), > ('circulation', 'AR_PROCESSING', '', 'Article request - processing', 0, 'Article request processing', 'Dear <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>),\r\n\r\nWe are now processing your request for an article from <<biblio.title>> (<<items.barcode>>).\r\n\r\nArticle requested:\r\nTitle: <<article_requests.title>>\r\nAuthor: <<article_requests.author>>\r\nVolume: <<article_requests.volume>>\r\nIssue: <<article_requests.issue>>\r\nDate: <<article_requests.date>>\r\nPages: <<article_requests.pages>>\r\nChapters: <<article_requests.chapters>>\r\nNotes: <<article_requests.patron_notes>>\r\n\r\nThank you!', 'email'), > ('circulation', 'CHECKOUT_NOTE', '', 'Checkout note on item set by patron', '0', 'Checkout note', '<<borrowers.firstname>> <<borrowers.surname>> has added a note to the item <<biblio.title>> - <<biblio.author>> (<<biblio.biblionumber>>).','email'); >- > INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`, `lang`) > VALUES > ('circulation', 'ACCOUNT_PAYMENT', '', 'Account payment', 0, 'Account payment', '[%- USE Price -%]\r\nA payment of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis payment affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'), > ('circulation', 'ACCOUNT_WRITEOFF', '', 'Account writeoff', 0, 'Account writeoff', '[%- USE Price -%]\r\nAn account writeoff of [% credit.amount * -1 | $Price %] has been applied to your account.\r\n\r\nThis writeoff affected the following fees:\r\n[%- FOREACH o IN offsets %]\r\nDescription: [% o.debit.description %]\r\nAmount paid: [% o.amount * -1 | $Price %]\r\nAmount remaining: [% o.debit.amountoutstanding | $Price %]\r\n[% END %]', 'email', 'default'); >+INSERT INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`) VALUES >+('circulation', 'SR_SLIP', '', 'Stock Rotation Slip', 0, 'Stockrotation Report', 'Stockrotation report for [% branch.name %]:\r\n\r\n[% IF branch.items.size %][% branch.items.size %] items to be processed for this branch.\r\n[% ELSE %]No items to be processed for this branch\r\n[% END %][% FOREACH item IN branch.items %][% IF item.reason ne \'in-demand\' %]Title: [% item.title %]\r\nAuthor: [% item.author %]\r\nCallnumber: [% item.callnumber %]\r\nLocation: [% item.location %]\r\nBarcode: [% item.barcode %]\r\nOn loan?: [% item.onloan %]\r\nStatus: [% item.reason %]\r\nCurrent Library: [% item.branch.branchname %] [% item.branch.branchcode %]\r\n\r\n[% END %][% END %]', 'email'); >diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql >index 4c5ecf2c0c..57ac0840e3 100644 >--- a/installer/data/mysql/kohastructure.sql >+++ b/installer/data/mysql/kohastructure.sql >@@ -4152,40 +4152,6 @@ CREATE TABLE illrequests ( > ON UPDATE CASCADE ON DELETE CASCADE > ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; > >--- >--- Table structure for table `stockrotationrotas` >--- >- >-CREATE TABLE IF NOT EXISTS stockrotationrotas ( >- rota_id int(11) auto_increment, -- Stockrotation rota ID >- title varchar(100) NOT NULL, -- Title for this rota >- description text NOT NULL default '', -- Description for this rota >- cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling? >- active tinyint(1) NOT NULL default 0, -- Is this rota currently active? >- PRIMARY KEY (`rota_id`) >-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; >- >--- >--- Table structure for table `stockrotationstages` >--- >- >-CREATE TABLE IF NOT EXISTS stockrotationstages ( >- stage_id int(11) auto_increment, -- Unique stage ID >- position int(11) NOT NULL, -- The position of this stage within its rota >- rota_id int(11) NOT NULL, -- The rota this stage belongs to >- branchcode_id varchar(10) NOT NULL, -- Branch this stage relates to >- duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage >- PRIMARY KEY (`stage_id`), >- CONSTRAINT `stockrotationstages_rifk` >- FOREIGN KEY (`rota_id`) >- REFERENCES `stockrotationrotas` (`rota_id`) >- ON UPDATE CASCADE ON DELETE CASCADE, >- CONSTRAINT `stockrotationstages_bifk` >- FOREIGN KEY (`branchcode_id`) >- REFERENCES `branches` (`branchcode`) >- ON UPDATE CASCADE ON DELETE CASCADE >-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; >- > -- > -- Table structure for table `illrequestattributes` > -- >@@ -4215,7 +4181,7 @@ CREATE TABLE library_groups ( > description MEDIUMTEXT NULL DEFAULT NULL, -- Longer explanation of the group, if necessary > ft_hide_patron_info tinyint(1) NOT NULL DEFAULT 0, -- Turn on the feature "Hide patron's info" for this group > ft_search_groups_opac tinyint(1) NOT NULL DEFAULT 0, -- Use this group for staff side search groups >- ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups >+ ft_search_groups_staff tinyint(1) NOT NULL DEFAULT 0, -- Use this group for opac side search groups > created_on TIMESTAMP NULL, -- Date and time of creation > updated_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Date and time of last > PRIMARY KEY id ( id ), >@@ -4236,6 +4202,40 @@ CREATE TABLE `oauth_access_tokens` ( > PRIMARY KEY (`access_token`) > ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; > >+-- >+-- Table structure for table `stockrotationrotas` >+-- >+ >+CREATE TABLE IF NOT EXISTS stockrotationrotas ( >+ rota_id int(11) auto_increment, -- Stockrotation rota ID >+ title varchar(100) NOT NULL, -- Title for this rota >+ description text NOT NULL default '', -- Description for this rota >+ cyclical tinyint(1) NOT NULL default 0, -- Should items on this rota keep cycling? >+ active tinyint(1) NOT NULL default 0, -- Is this rota currently active? >+ PRIMARY KEY (`rota_id`) >+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; >+ >+-- >+-- Table structure for table `stockrotationstages` >+-- >+ >+CREATE TABLE IF NOT EXISTS stockrotationstages ( >+ stage_id int(11) auto_increment, -- Unique stage ID >+ position int(11) NOT NULL, -- The position of this stage within its rota >+ rota_id int(11) NOT NULL, -- The rota this stage belongs to >+ branchcode_id varchar(10) NOT NULL, -- Branch this stage relates to >+ duration int(11) NOT NULL default 4, -- The number of days items shoud occupy this stage >+ PRIMARY KEY (`stage_id`), >+ CONSTRAINT `stockrotationstages_rifk` >+ FOREIGN KEY (`rota_id`) >+ REFERENCES `stockrotationrotas` (`rota_id`) >+ ON UPDATE CASCADE ON DELETE CASCADE, >+ CONSTRAINT `stockrotationstages_bifk` >+ FOREIGN KEY (`branchcode_id`) >+ REFERENCES `branches` (`branchcode`) >+ ON UPDATE CASCADE ON DELETE CASCADE >+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; >+ > -- > -- Table structure for table `stockrotationitems` > -- >@@ -4254,7 +4254,7 @@ CREATE TABLE IF NOT EXISTS stockrotationitems ( > FOREIGN KEY (`stage_id`) > REFERENCES `stockrotationstages` (`stage_id`) > ON UPDATE CASCADE ON DELETE CASCADE >-) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; >+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; > > /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; > /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; >diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql >index 4e7569cdfb..88f1d19f9f 100644 >--- a/installer/data/mysql/sysprefs.sql >+++ b/installer/data/mysql/sysprefs.sql >@@ -475,6 +475,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('reviewson','1','','If ON, enables patron reviews of bibliographic records in the OPAC','YesNo'), > ('RisExportAdditionalFields', '', NULL , 'Define additional RIS tags to export from MARC records in YAML format as an associative array with either a marc tag/subfield combination as the value, or a list of tag/subfield combinations.', 'textarea'), > ('RoutingListAddReserves','0','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'), >+('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo'), >+('RoutingListAddReserves','1','','If ON the patrons on routing lists are automatically added to holds on the issue.','YesNo'), > ('RoutingListNote','To change this note edit <a href=\"/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=RoutingListNote#jumped\">RoutingListNote</a> system preference.','70|10','Define a note to be shown on all routing lists','Textarea'), > ('RoutingSerials','1',NULL,'If ON, serials routing is enabled','YesNo'), > ('SCOMainUserBlock','','70|10','Add a block of HTML that will display on the self checkout screen','Textarea'), >@@ -521,6 +523,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('StaffSerialIssueDisplayCount','3','','Number of serial issues to display per subscription in the Staff client','Integer'), > ('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'), > ('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'), >+('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'), > ('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'), > ('SubfieldsToAllowForRestrictedBatchmod','','Define a list of subfields for which edition is authorized when items_batchmod_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), > ('SubfieldsToAllowForRestrictedEditing','','Define a list of subfields for which edition is authorized when edit_items_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), >@@ -611,7 +614,5 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'), > ('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'), > ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'), >-('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'), >-('StockRotation','0',NULL,'If ON, enables the stock rotation module','YesNo'), >-('RotationPreventTransfers','0',NULL,'If ON, prevent any transfers for items on stock rotation rotas, except for stock rotation transfers','YesNo') >+('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo') > ; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index 2fc5b84410..b1296c45d0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -4026,6 +4026,127 @@ span { > width: 100% !important; > } > >+#stockrotation { >+ h3 { >+ margin: 30px 0 10px 0; >+ } >+ .dialog { >+ h3 { >+ margin: 10px 0; >+ } >+ margin-bottom: 20px; >+ } >+ .highlight_stage { >+ font-weight: bold; >+ } >+} >+ >+#catalog_stockrotation .highlight_stage { >+ font-weight: bold; >+} >+ >+#stockrotation { >+ #rota_form { >+ textarea { >+ width: 300px; >+ height: 100px; >+ } >+ #name { >+ width: 300px; >+ } >+ fieldset { >+ width: auto; >+ } >+ } >+ #stage_form fieldset, #add_rota_item_form fieldset { >+ width: auto; >+ } >+ .dialog.alert { >+ ul { >+ margin: 20px 0; >+ } >+ li { >+ list-style-type: none; >+ } >+ } >+} >+ >+#catalog_stockrotation { >+ .item_select_rota { >+ vertical-align: middle; >+ } >+ h1 { >+ margin-bottom: 20px; >+ } >+} >+ >+#stockrotation td.actions, #catalog_stockrotation td.actions { >+ vertical-align: middle; >+} >+ >+#stockrotation .stage, #catalog_stockrotation .stage { >+ display: inline-block; >+ padding: 5px 7px; >+ margin: 3px 0 3px 0; >+ border-radius: 5px; >+ background-color: rgba(0, 0, 0, 0.1); >+} >+ >+#stage_list_headings { >+ font-weight: bold; >+ span { >+ padding: 3px; >+ } >+} >+ >+#manage_stages { >+ ul { >+ padding-left: 0; >+ } >+ li { >+ list-style: none; >+ margin-bottom: 5px; >+ span { >+ padding: 6px 3px; >+ } >+ } >+ .stagename { >+ width: 15em; >+ display: inline-block; >+ } >+ .stageduration { >+ width: 10em; >+ display: inline-block; >+ } >+ .stageactions { >+ display: inline-block; >+ } >+ li:nth-child(odd) { >+ background-color: #F3F3F3; >+ } >+ .drag_handle { >+ margin-right: 6px; >+ cursor: move; >+ } >+ .drag_placeholder { >+ height: 2em; >+ border: 1px dotted #aaa; >+ } >+ h3 { >+ display: inline-block; >+ } >+ #ajax_status { >+ display: inline-block; >+ border: 1px solid #bcbcbc; >+ border-radius: 5px; >+ padding: 5px; >+ margin-left: 10px; >+ background: #f3f3f3; >+ } >+ #manage_stages_help { >+ margin: 20px 0; >+ } >+} > > #helper { > span { >diff --git a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >index 0bd083acfe..9864e91bc7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >+++ b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css >@@ -1 +1 @@ >-@charset "UTF-8";@import url("../../lib/yui/reset-fonts-grids.css") screen;::-moz-selection{background:#538200;color:#fff}::selection{background:#538200;color:#fff}a:link,a:visited{color:#004d99;text-decoration:none}a:active,a:hover{color:#538200;text-decoration:none}a:hover .term{color:#ff9090}a.btn:link,a.btn:visited{color:#333}a.btn.btn-link:link,a.btn.btn-link:visited{color:#004d99}a.btn.btn-link:hover{color:#538200}a.cancel{padding-left:1em}a.cartRemove{color:#c33;font-size:90%;margin:0;padding:0}a.close:hover{color:#538200}a.csv{background-image:url(../img/famfamfam/silk/page_white_excel.png)}a.dropdown-toggle{white-space:nowrap}a.incart{color:#666}a.debit,a.overdue{color:#c00}a.popup{background:transparent url(../img/pop-up-link.png) 100% no-repeat;padding-right:15px}a.disabled{color:#999}a.document{background-position:0 middle;background-repeat:no-repeat;display:inline-block;min-height:20px;padding-left:20px}a.highlight_toggle{display:none}a .localimage img{border:1px solid #00c;margin:0 .5em;padding:.3em}a.pdf{background-image:url(../img/famfamfam/silk/page_white_acrobat.png)}a.submit{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em;display:inline-block}a.submit:active{border:1px inset #999}a.submit:disabled{background:#eee none;border:1px solid silver;color:#999}a.term{text-decoration:underline}a.xml{background-image:url(../img/famfamfam/silk/page_white_code.png)}aside h5{font-size:100%;margin:.5em 0}aside fieldset.brief{margin:0;padding:.4em .7em}aside fieldset.brief fieldset{margin:0;padding:.5em 0}aside fieldset.brief fieldset legend{font-size:85%}aside fieldset.brief li.checkbox label,aside fieldset.brief li.dateinsert label,aside fieldset.brief li.dateinsert span.label{display:inline}aside fieldset.brief li.radio{padding:.7em 0}aside fieldset.brief li.radio input{padding:.3em 0}aside fieldset.brief li.radio label,aside fieldset.brief li.radio span.label{display:inline}aside fieldset.brief ol{font-size:85%;margin:0;padding:0}aside fieldset.brief [type=text],aside fieldset.brief select{width:100%}button{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}button:active{border:1px inset #999}button:disabled{background:#eee none;border:1px solid silver;color:#999}button.closebtn{background:transparent;border:0;cursor:pointer;padding:0}main .yui-b fieldset.brief [type=text],main .yui-b fieldset.brief select{width:auto}table{border-collapse:collapse;border-right:1px solid #bcbcbc;border-top:1px solid #bcbcbc}table .btn-group{white-space:nowrap}table .btn-group .btn{display:inline-block;float:none}table.indexes td{vertical-align:middle}table>caption span.actions{font-size:66%;font-weight:400;margin:0 .5em 0 0}table.invis,table.invis td,table.invis tr{border:0}table+table{margin-top:1em}td,th{border-bottom:1px solid #bcbcbc;border-left:1px solid #bcbcbc;padding:.2em .3em}td{background-color:#fff;vertical-align:top}td.actions{white-space:nowrap}td.borderless{border:0 none;border-collapse:separate}td.data{font-family:Courier New,Courier,monospace}td.data,td.total{text-align:right}td input.approve{background-color:#ffc}th{background-color:#e8e8e8;font-weight:700;text-align:center}th.data{font-family:Courier New,Courier,monospace;text-align:right}body{font-family:Arial,Verdana,Helvetica,sans-serif;padding:0 0 4em;text-align:left}br.clear{clear:both;line-height:1px}form{display:inline}form.confirm{display:block;text-align:center}h1{font-size:161.6%;font-weight:700}h1#logo{border:0 none;float:left;margin:.75em .3em .75em .7em;padding:0;width:180px}h2{font-size:146.5%}h2,h3{font-weight:700}h3{font-size:131%}h4{font-size:116%}h4,h5{font-weight:700}h5{font-size:100%}h6{font-size:93%;font-weight:700}h1,h2,h3,h4,h5,h6{margin:.3em 0}hr{clear:both}p{margin:.5em 0}strong{font-weight:700}em strong,strong em{font-style:italic;font-weight:700}cite,em{font-style:italic}input,textarea{line-height:normal;padding:2px 4px}input:focus,textarea:focus{border-color:#538200;border-radius:4px;border-style:solid}input[type=checkbox],input[type=radio]{margin:0;vertical-align:middle}input[type=button]:active,input[type=submit]:active{border:1px inset #999}input[type=button],input[type=reset],input[type=submit]{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}input[type=button]:active,input[type=reset]:active,input[type=submit]:active{border:1px inset #999}input[type=button]:disabled,input[type=reset]:disabled,input[type=submit]:disabled{background:#eee none;border:1px solid silver;color:#999}input.alert{background-color:#ff9;border-color:#900}input.submit{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}input.submit:active{border:1px inset #999}input.submit:disabled{background:#eee none;border:1px solid silver;color:#999}input.warning{background:#fff url(../img/famfamfam/silk/error.png) no-repeat 4px;padding:.25em .25em .25em 25px}.label,label{color:#000;display:inline;font-size:inherit;font-weight:400;max-width:inherit;padding:0;vertical-align:middle}.label input[type=checkbox],.label input[type=radio],label input[type=checkbox],label input[type=radio]{margin-top:0}.label.circ_barcode,label.circ_barcode{font-size:105%;font-weight:700}.label.permissioncode,label.permissioncode{font-style:italic}.label.permissioncode:before,label.permissioncode:before{content:"("}.label.permissioncode:after,label.permissioncode:after{content:")"}.label.required,label.required{color:#c00}.subfield-label{font-style:italic}.subfield-label span.subfield-code{font-weight:700}.members-update-table{padding-top:10px}#navmenulist li{border-bottom:1px solid #eee;list-style-image:url(../img/arrow-bullet.gif);padding:.2em 0}#navmenulist li a{text-decoration:none}#navmenulist li a.current{font-weight:700}#doc,#doc1,#doc2,#doc3{padding-top:1em}.main{margin-top:1em}#login_controls{padding:.4em .5em;position:absolute;right:.5em}ul{padding-left:1.1em}ul li{list-style-type:disc}ul li input.submit{font-size:87%;padding:2px}ul li li{list-style-type:circle}ul .toolbar{padding-left:0}ul .toolbar button{font-family:Arial,Verdana,Helvetica,sans-serif;padding-bottom:2px}ul .toolbar li{display:inline;list-style:none}ul.budget_hierarchy{margin-left:0;padding-left:0}ul.budget_hierarchy li{display:inline}ul.budget_hierarchy li:after{content:" -> "}ul.budget_hierarchy li:first-child:after,ul.budget_hierarchy li:last-child:after{content:""}ul.fa-ul li{list-style-type:none}ul.ui-tabs-nav li{list-style:none}ol{padding-left:1.5em}ol li{list-style:decimal}ol.bibliodetails{float:left;margin:0 0 1em 1em}ol.bibliodetails li{border-bottom:1px solid #e8e8e8;list-style-type:none;padding:.1em}ol.bibliodetails span.label{border-right:1px solid #e8e8e8;float:left;font-weight:700;margin-right:1em;width:12em}.gradient{background-image:linear-gradient(180deg,#e6f0f2 1%,#fff 99%);display:inline-block;width:100%}.cart-controls{border-top:1px solid #e8e8e8;padding:7px 0}#editions table,#editions td{border:0}.highlighted-row,.highlighted-row td{background-color:#ffd000!important}tbody tr:nth-child(odd) td{background-color:#f3f3f3;border:1px solid #bcbcbc;border-right:1px solid #bcbcbc}.overdue td.od{color:#c00;font-weight:700}tr.clickable{cursor:pointer}tr.expired td{color:#999}tr.highlight td{background-color:#f6f6f6;border-color:#bcbcbc}tr.highlight th[scope=row]{background-color:#ddd;border-color:#bcbcbc}tr.highlight table.invis td{border:0}tr.odd.onissue td{background-color:#ffffe1}tr.ok:nth-child(2n) td,tr.ok:nth-child(odd) td,tr.ok td,tr.onissue td{background-color:#ffc}tr.reserved td{background-color:#eeffd4}tr.transfered td{background-color:#e8f0f6}tr.warn:nth-child(odd) td,tr.warn td{background-color:#ff9090}.table_borrowers tr:hover td{background-color:#ff9}tfoot td{background-color:#f3f3f3;font-weight:700}caption{color:#000;font-size:133.9%;font-weight:700;margin:.3em 0}.problem{background-color:#ffc;color:#900;font-weight:700;line-height:1.7em}fieldset{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;margin:1em 1em 1em 0;padding:1em}fieldset+fieldset.action{padding-top:20px}fieldset .lastchecked{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom-width:0;margin-bottom:0}fieldset.action{background-color:transparent;border:0;clear:both;float:none;margin:.9em 0 0;padding:.4em;width:auto}fieldset.brief{border:2px solid #b9d8d9}fieldset.brief div.hint{margin-bottom:.4em}fieldset.brief label{display:block;font-weight:700;padding:.3em 0}fieldset.brief label.inline{display:inline;float:none;margin-left:1em;width:auto}fieldset.brief li[aria-disabled=true]{color:#999}fieldset.brief li.inline{display:inline;float:none;margin-left:1em;width:auto}fieldset.brief li,fieldset.brief ol{list-style-type:none}fieldset.brief span .label{display:block;font-weight:700;padding:.3em 0;text-align:left}fieldset.rows{border:2px solid #b9d8d9;border-width:1px;clear:left;float:left;font-size:90%;margin:.9em 0 0;padding:0;width:100%}fieldset.rows fieldset{background-color:transparent;border-width:1px;margin:1em;padding:.3em}fieldset.rows fieldset.action{padding:1em}fieldset.rows.inputnote{clear:left;float:left;margin:1em 0 0 11em}fieldset.rows.left label{text-align:left;width:8em}fieldset.rows.left li{padding-bottom:.4em}fieldset.rows.left span label{text-align:left;width:8em}fieldset.rows.ui-accordion-content{border-top-left-radius:0;border-top-right-radius:0;margin:0;padding:0;width:auto}fieldset.rows.ui-accordion-content table{margin:0}fieldset.rows.unselected{background-color:#fff;border:0;border-width:0}fieldset.rows caption{font-size:120%}fieldset.rows div.hint{margin-bottom:.4em;margin-left:10.5em}fieldset.rows label{float:left;font-weight:700;margin-right:1em;text-align:right;width:9em}fieldset.rows label.error{float:none;margin-left:1em;width:auto}fieldset.rows label.inline{display:inline;float:none;margin-left:1em}fieldset.rows label.yesno{float:none;width:auto}fieldset.rows legend{font-size:110%;font-weight:700;margin-left:1em}fieldset.rows li{clear:left;float:left;list-style-type:none;padding-bottom:1em;width:100%}fieldset.rows li[aria-disabled=true]{color:#999}fieldset.rows li.radio{padding-left:9em;width:auto}fieldset.rows li.radio input+label{margin-left:0;padding-left:0}fieldset.rows li.radio label{float:none;margin:0 0 0 1em;width:auto}fieldset.rows li input+label{margin-left:0;padding-left:0}fieldset.rows ol{list-style-type:none;padding:1em 1em 0}fieldset.rows ol.radio label{float:none;margin-left:20px;margin-right:30px;padding-left:0;vertical-align:middle;width:auto}fieldset.rows ol.radio label.radio{float:left;margin-right:1em;margin-top:0;width:9em}fieldset.rows ol.radio input[type=checkbox],fieldset.rows ol.radio input[type=radio]{margin-left:-20px}fieldset.rows p{margin:1em 0 1em 1em}fieldset.rows span.label{float:left;font-weight:700;margin-right:1em;text-align:right;width:9em}fieldset.rows table{clear:both;font-size:105%;margin:1em 0 1em 1em}fieldset.rows table.mceListBox{margin:0}fieldset.rows td label{float:none;font-weight:400;width:auto}fieldset.rows .inputnote{clear:left;float:left;margin:1em 0 0 11em}fieldset.rows+h3{clear:both;padding-top:.5em}#multi_receiving fieldset.rows label{width:50%}.yui-u div.hint{margin-bottom:.4em}.yui-u fieldset.rows div.hint{margin-left:7.5em}.yui-u fieldset.rows label,.yui-u fieldset.rows span.label{width:10em}.yui-u .rows li p label.widelabel,legend{width:auto}legend{background-color:#fff;border:2px solid #b9d8d9;border-radius:3px;font-size:123.1%;font-weight:700;padding:.2em .5em}details>summary{cursor:pointer}details>summary:before{content:"\f0da";display:inline-block;font-family:FontAwesome;width:1em}details>summary.checkouts-by-itemtype li{display:inline-block}details[open]>summary:before{content:"\f0d7"}#floating-save{background-color:rgba(185,216,217,.6);bottom:3%;position:fixed;right:1%;width:150px}#breadcrumbs{background-color:#e6f0f2;clear:both;font-size:90%;margin:0;padding:.2em .5em .4em 10px}#header.navbar{margin-bottom:0}#header.navbar-default{background:#e6f0f2;border:0;box-shadow:none}#header+#breadcrumbs{margin-top:1em}#header>.container-fluid{padding:0}div.action{background-color:transparent;border:0;clear:both;float:none;margin:.9em 0 0;padding:.4em;width:auto}div .circmessage{margin-bottom:.3em;padding:0 .4em .4em}div .circmessage:first-child{margin-top:1em}div.error{background-color:#ff9;border:2px dashed #900;margin:1em;padding:.5em}div.first fieldset{margin-right:0}div.help{margin:.9em 0 0}div.justify{text-align:justify}div.message{background:linear-gradient(180deg,#fff 0,#f4f6fa 2%,#eaeef5 23%,#e8edf6 94%,#cddbf2);border:1px solid #bcbcbc;text-align:center;width:55%}div.message h5,div.message ul{padding-left:25%;text-align:left}div.message ul+h4{margin-top:.7em}div.note{background:linear-gradient(180deg,#f4f6fa 0,#e8edf6);border:1px solid #bcbcbc;margin:.5em 0;padding:.5em}div.note i.fa-exclamation{color:#c00;font-style:italic;padding:0 .3em}div.rules{display:block}div.results,div[class$=_table_controls]{padding:.7em 0}div.rule{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;margin:.3em;padding:.3em}div.lastchecked{border:2px solid #bcdb89;border-bottom-left-radius:5px;border-bottom-right-radius:5px;padding:.2em 1em}div.listgroup{clear:left}div.listgroup h4{font-style:italic}div.listgroup h4 a,div.listgroup input{font-size:80%}div.sysprefs h3{margin:.2em 0 .2em .4em}div.sysprefs dl{margin-left:1.5em}div.sysprefs.hint{float:right;margin:.7em;padding:.5em;width:25%}div.rows{clear:left;float:left;margin:0;padding:0;width:100%}div.rows+div.rows{margin-top:.6em}div.rows li{border-bottom:1px solid #eee;clear:left;float:left;list-style-type:none;padding-bottom:.2em;padding-top:.1em;width:100%}div.rows ol{list-style-type:none;padding:.5em 1em 0 0}div.rows ol li li{border-bottom:0}div.rows span.label{float:left;font-weight:700;margin-right:1em;padding-top:0;text-align:left;width:9em}div.pages{margin:.5em 0}div.pages a{font-weight:700;padding:1px 5px;text-decoration:none}div.pages a:link,div.pages a:visited{background-color:#eee;color:#36c}div.pages a:active,div.pages a:hover{background-color:#ffc}div.pages .current,div.pages .currentPage{background-color:#e6fcb7;color:#666;font-weight:700;padding:1px 5px}div.pages .inactive{background-color:#f3f3f3;color:#bcbcbc;font-weight:700;padding:1px 5px}div .browse{margin:.5em 0}#header_search{background-position:.5em .5em;background-repeat:no-repeat;float:left;margin:.3em 0 .5em}#header_search input{font-size:1.3em}#header_search input.submit{font-size:1em}#header_search div.residentsearch{border:0;border-bottom:1px solid #85ca11;padding:0 0 .2em}#header_search ul.ui-tabs-nav{margin-left:1em;padding-top:0}#header_search ul.ui-tabs-nav li.ui-state-default{background:transparent none;border:0;top:0}#header_search ul.ui-tabs-nav li.ui-state-default a{padding:.3em .6em}#header_search ul.ui-tabs-nav li.ui-tabs-active{background-color:#fffff1;border:1px solid #85ca11;border-top-width:0;top:-2px}#header_search ul.ui-tabs-nav li.ui-tabs-active a{text-decoration:none}#header_search .ui-corner-top{border-radius:0 0 4px 4px}#header_search>div,#header_search>div>li,#header_search>ul,#header_search>ul>li{display:none}#header_search>div:first-of-type,#header_search>div>li:first-of-type,#header_search>ul:first-of-type,#header_search>ul>li:first-of-type{display:block}.head-searchbox{width:30em}#checkouts,#reserves{border:1px solid #b9d8d9;padding:1em}.tip{color:gray;font-size:93%}.single-line{white-space:nowrap}.ex{font-family:Courier New,Courier,monospace}.ex,dt{font-weight:700}dd{font-size:90%;font-weight:400;padding:.2em;text-indent:2.5em}#toolbar,.btn-toolbar{background-color:#edf4f6;border:1px solid #e6f0f2;border-radius:5px 5px 0 0;margin:0;padding:5px}#disabled2 a,#disabled a,#disabled a:hover{color:#999}.patroninfo{margin-top:-.5em}.patroninfo h5{border-right:1px solid #b9d8d9;margin-bottom:0;padding-bottom:.5em;padding-left:-.5em;padding-top:.3em}.patroninfo h5:empty{border-right:0}.patroninfo ul{border:0;border-bottom:0;border-right:1px solid #b9d8d9;border-top:0;margin:0;padding:0}.patroninfo ul li{list-style-type:none;margin:0}.patroninfo+#menu{margin-right:0}#patronbasics div{background:transparent url(../img/patron-blank.min.svg) 10px 5px no-repeat;height:125px;padding:0;width:105px}#patronbasics div,#patronimage{border:1px solid #ccc;margin:.3em 0 .3em .3em}#patronimage{max-width:140px;padding:.2em}.patronviews{border-right:1px solid #000;border-top:1px solid #000;margin-bottom:.5em;padding:.5em 0}.column-tool{font-size:80%}.hint{color:#666;font-size:95%}.yui-b fieldset.brief{padding:.4em .7em}.yui-b fieldset.brief fieldset{margin:0 .3em;padding:.5em}.yui-b fieldset.brief fieldset legend{font-size:85%}#tools_holidays .yui-b fieldset.brief li.checkbox input{margin-left:0}.yui-b fieldset.brief li.checkbox label{display:inline}#tools_holidays .yui-b fieldset.brief li.checkbox label{margin-left:20px}.yui-b fieldset.brief li.dateinsert label,.yui-b fieldset.brief li.dateinsert span.label{display:inline}.yui-b fieldset.brief li.radio{padding:.7em 0}.yui-b fieldset.brief li.radio input{padding:.3em 0}#tools_holidays .yui-b fieldset.brief li.radio input{margin-left:0}.yui-b fieldset.brief li.radio label{display:inline}#tools_holidays .yui-b fieldset.brief li.radio label{margin-left:20px}.yui-b fieldset.brief li.radio label span.label{display:inline}.yui-b fieldset.brief ol{font-size:85%;margin:0;padding:0}.yui-b fieldset.brief [type=text],.yui-b fieldset.brief select{width:100%}.yui-b fieldset.rows div.hint{margin-left:10.5em}#yui-main .yui-b fieldset.brief [type=text],#yui-main .yui-b fieldset.brief select,.yui-b fieldset.rows td label,.yui-b fieldset.rows td span.label{width:auto}.btn-toolbar fieldset.action{margin-top:0}.btn-toolbar .dropdown-menu{font-size:13px}.rows .label{white-space:normal}.checkedout{color:#999;font-style:italic}.subfield_not_filled{background-color:#ff9}.content_hidden{display:none;visibility:hidden}.content_visible{display:block;visibility:visible}#z3950searcht table{border:0;padding:20px}#z3950_search_targets{height:338px;overflow-y:auto}#z3950_search_targets_acq{height:308px;overflow-y:auto}.z3950checks{padding-left:1em}.error{color:#c00}.status_ok{background-color:#90ee90}.status_warn{background-color:red}i.error{color:#c00}i.success{color:green}i.warn{color:orange}.checkout-setting{font-size:85%;padding-top:.3em}.checkout-setting input{vertical-align:middle}.checkout-setting label{font-size:inherit;font-weight:400}.checkout-settings{background-color:#f4f8f9;border-radius:0;border-top:2px solid #b9d8d9;display:none;margin-left:-1em;margin-right:-1em;margin-top:1em;padding:1em 1em 0}#show-checkout-settings{margin-top:.5em}.blocker,.inaccurate-item-statuses{color:#900}.circmessage li{list-style:url(../img/arrow-bullet.gif);margin-bottom:.2em}#circ_needsconfirmation{margin:auto}.dialog{border:1px solid #bcbcbc;border-radius:2px;margin:1em auto;padding:.5em;text-align:center;width:65%}.dialog a.approve{display:inline-block}.dialog a.approve,.dialog button{background:#fff none;border:1px outset #999;border-left-color:#666;border-top-color:#666;margin:.4em;padding:.4em;white-space:pre-line}.dialog a.approve:active,.dialog button:active{border:1px inset #999}.dialog a.approve:hover,.dialog button:hover{background-color:#ffc}.dialog h2,.dialog h3,.dialog h4{margin:auto;text-align:center}.dialog input{background-color:#fff;border:1px solid #bcbcbc;margin:.4em;padding:.4em .4em .4em 25px}.dialog input:hover{background-color:#ffc}.dialog input[type=submit]{background:#fff none}.dialog li{list-style-position:inside}.dialog table{margin:.5em auto}.dialog table td{text-align:left}.dialog table th{text-align:right}.alert{background:linear-gradient(180deg,#fef8d3 0,#ffec91 9%,#ffed87 89%,#f9dc00);border:1px solid #e0c726;color:inherit;text-align:center;text-shadow:none}.alert strong{color:#900}.alert .closebtn{line-height:20px;position:relative;right:-21px;top:-2px}.approve i.fa,.success i.fa{color:green}.deny i.fa{color:#c00}.new i.fa{color:#425faf}.accesskey{text-decoration:underline}.missing,.term{background-color:#ffc}.term{color:#900}.shelvingloc{display:block;font-style:italic}#menu{border-right:1px solid #b9d8d9;margin-right:.5em;padding-bottom:2em;padding-top:1em}#menu li a{background:linear-gradient(180deg,#e8f0f6 0,#e8f0f6 96%,#c1c1c1);border:1px solid #b9d8d9;border-bottom-left-radius:5px;border-top-left-radius:5px;display:block;font-size:111%;margin:.5em 0;margin-right:-1px;padding:.4em .3em;text-decoration:none}#menu li a:hover{background:linear-gradient(180deg,#fafafa 0,#fff 96%,#e6e6e6 97%,#ccc 99%,#c1c1c1)}#menu li.active a,#menu li a:hover{border-bottom:1px solid #85ca11;border-left:1px solid #85ca11;border-top:1px solid #85ca11}#menu li.active a{background-color:#fff;background-image:none;border-right:0;color:#000;font-weight:700}#menu li.active a:hover{background-color:#fff;color:#538200}#menu ul li{list-style-type:none}#logo{background:transparent url(../img/koha-logo-medium.png) no-repeat scroll 0;margin:.75em .3em .75em .7em}#logo a{border:0;cursor:pointer;display:block;height:0!important;margin:0;overflow:hidden;padding:44px 0 0;text-decoration:none;width:180px}#closewindow{margin-top:2em;text-align:center}#closewindow a{font-weight:700}.barcode{font-size:200%;vertical-align:middle}li.email{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.patronbriefinfo li.email{font-size:87%;padding:0 10px 0 0;width:90%}.empty{color:#ccc}.address{font-size:110%}.address li{list-style-type:none}.title{font-size:105%;font-weight:700}.hold{float:right;font-size:90%;margin:0}.thumbnail{display:block;margin:auto}.thumbnail>li{list-style-type:none}#searchresults ul li{clear:left;font-size:90%;list-style:url(../img/item-bullet.gif);padding:.2em 0}#searchresults ul li img{float:left;margin:3px 5px 3px -5px}#searchresults ul span.status{clear:left;color:#900;display:block}#searchresults ul span.unavailable{clear:left;display:block}#searchresults ul table td{vertical-align:top}#searchresults.unavailability strong{display:block}#searchheader{background-color:#e6f0f2;border:1px solid #b9d8d9;border-radius:5px 5px 0 0;font-size:80%;margin:0 0 .5em -1px;padding:.4em 0 .4em 1em}#searchheader.floating{border-radius:0;margin-top:0}#searchheader .btn-group>.btn:first-child{margin-left:.7em}#searchheader form{float:right;padding:5px 5px 3px 0}#searchheader form.fz3950{float:right;font-size:125%;padding:0 0 0 5em}#searchheader form.fz3950bigrpad{float:right;font-size:125%;padding:5px 25em 0 0}#search-facets{border:1px solid #b9d8d9;border-radius:5px 5px 0 0}#search-facets h4{background-color:#e6f0f2;border-bottom:1px solid #b9d8d9;border-radius:5px 5px 0 0;font-size:90%;margin:0;padding:.4em .2em;text-align:center}#search-facets ul{margin:0;padding:.3em}#search-facets ul li{font-weight:700;list-style-type:none}#search-facets li li{font-size:85%;font-weight:400;margin-bottom:2px;padding:.1em .2em}#search-facets li.showmore{font-weight:700;text-indent:1em}.facet-count{display:inline-block}#bookcoverimg{text-align:center}.searchhighlightblob{font-size:75%;font-style:italic}#displayexample{background-color:#ccc;margin-bottom:10px;padding:5px}#irregularity_summary{vertical-align:top}#toplevelmenu{padding:0}#CheckAll,#CheckNone,#CheckPending{font-weight:400;margin:0 .5em 0 0}.dmg,.lost,.wdn{color:#900;display:block}.datedue{color:#999;display:block;font-style:italic}.credit,.waitinghere{color:#690}#mainuserblock{border:1px solid #e8e8e8;margin-top:.5em;padding:.5em}.labeledmarc-table{border:0}.labeledmarc-label{border:0;color:#000;font-size:11pt;font-style:italic;padding:5}.labeledmarc-value{border:0;color:#000;font-size:10pt;padding:5}#marcPreview table{border:0;font-family:Courier New,Courier,monospace;font-size:95%;margin:.7em 0 0}#marcPreview tbody tr:nth-child(odd) td{background-color:#fff}#marcPreview td,#marcPreview th{border:0;padding:2px;vertical-align:top}#marcPreview th{background-color:#fff;text-align:left;white-space:nowrap}#marcPreview.modal-dialog,.modal-dialog.modal-wide{width:80%}@media (max-width:767px){#marcPreview{margin:0;width:auto}}#cartDetails{background-color:#fff;border:1px solid #739acf;box-shadow:1px 1px 3px 0 #666;color:#000;display:none;margin:0;padding:10px;text-align:center;width:180px;z-index:2}#cartmenulink{background:transparent url(../img/cart-small.gif) 0 no-repeat;padding-left:15px}#basketcount span{display:inline;font-size:90%;font-weight:400;padding:0}#moremenu{display:none}.results_summary{color:#707070;display:block;font-size:85%;padding:0 0 .5em}.results_summary a{font-weight:400}.results_summary .label{color:#202020}.child_fund_amount{font-style:italic}.number_box{font-size:105%;line-height:200%}.number_box a,.number_box span{background-color:#e4ecf5;border:1px solid #a4bedd;border-radius:4px;font-weight:700;padding:.1em .4em;text-decoration:none}.number_box a:hover,.number_box span:hover{background-color:#ebeff7}.container{border:1px solid #eee;margin:1em 0;padding:1em}.import_export{position:relative}.import_export .export_ok{background:#e3e3e3 none;border:0;cursor:pointer;margin-left:20px;padding:10px}.import_export .import_export_options{background:#fff;border:1px solid #cdcdcd;left:60px;padding:10px;position:absolute;top:0;width:300px;z-index:1}.import_export_options{background:#e3e3e3 none;border:0;cursor:pointer;margin-left:20px;padding:10px}.import_export_options fieldset.rows li label{width:16em}.import_export_options .importing{background:none;padding:inherit}.form_import fieldset.rows li label{width:auto}.form_import .input_import{border:1px solid #bcbcbc}.importing{position:relative}.importing .importing_msg{padding-bottom:10px;padding-left:10px}.field_hint{color:gray;font-style:italic;padding-left:1em}.m880{display:block;float:right;padding-left:20px;text-align:right;width:50%}.advsearch{margin:0}.advsearch table{border-collapse:separate;border-spacing:5px;border-width:0}.advsearch td{border:1px solid #eee;padding:.3em .4em}#circ_circulation_issue{position:relative}#clearscreen{position:absolute;right:0;top:0}#clearscreen a{background-color:#eee;border-radius:0 0 0 5px;color:#ccc;display:block;font-size:160%;font-weight:700;padding:0 .7em .2em;text-decoration:none;text-shadow:0 -1px 0 #666}#clearscreen a:hover{color:#c00}.pager{background-color:#e8e8e8;border:1px solid #bcbcbc;border-radius:5px;display:inline-block;font-size:85%;margin:.4em 0;padding:.3em .5em}.pager img{vertical-align:middle}.pager img.last{padding-right:5px}.pager input.pagedisplay{background-color:transparent;border:0;font-weight:700;text-align:center}.pager p{margin:0}.no-image{background-color:#fff;border:1px solid #aaa;border-radius:3px;color:#979797;display:block;font-size:86%;font-weight:700;text-align:center;width:75px}#acqui_order_supplierlist>div.supplier{border:1px solid #eee;margin:.5em;padding:1em}#acqui_order_supplierlist>div>div>.baskets{margin-top:.5em}#acqui_order_supplierlist>div>span.action{margin-left:5em}#acqui_order_supplierlist>div>span.suppliername{display:inline;font-size:1.7em;margin-bottom:.5em}.supplier-contact-details{float:left}#ADD-contact{margin:0 0 8px 8px}#contact-template{display:none}.ui-widget-content{background:#fff none;border:1px solid #b9d8d9;color:#222}.ui-widget-header{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#222;font-weight:700}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{background:#f4f8f9 none;border:1px solid #b9d8d9;color:#555;font-weight:400}.ui-state-focus,.ui-state-hover,.ui-widget-content .ui-state-focus,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-focus,.ui-widget-header .ui-state-hover{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#212121;font-weight:400}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{background:#fff none;border:1px solid #aaa;color:#212121;font-weight:400}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{background:#fff4c6;border:1px solid #fed22f;color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{background:#fef1ec;border:1px solid #cd0a0a;color:#cd0a0a}.ui-autocomplete{box-shadow:2px 2px 2px rgba(0,0,0,.3);cursor:default;position:absolute}.ui-autocomplete.ui-widget-content .ui-state-hover{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#212121;font-weight:400}.ui-autocomplete-loading{background:#fff url(../img/spinner-small.gif) 100% no-repeat}.ui-menu li{list-style:none}.ui-tabs-nav .ui-tabs-active a,.ui-tabs-nav a:active,.ui-tabs-nav a:focus,.ui-tabs-nav a:hover,.ui-tabs-nav span.a{background:none repeat scroll 0 0 transparent;outline:0 none}.ui-tabs-nav.ui-widget-header{background:none;border:0}.ui-tabs .ui-tabs-nav li{background:#e6f0f2 none;border:1px solid #b9d8d9;margin-right:.4em;top:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active{background-color:#fff;border:1px solid #b9d8d9;border-bottom-width:0}.ui-tabs .ui-tabs-nav li.ui-tabs-active a{color:#000;font-weight:700}.ui-tabs .ui-tabs-nav li.ui-tabs-active.ui-state-hover{background:#fff none}.ui-tabs .ui-tabs-nav li.ui-state-default.ui-state-hover{background:#edf4f5 none}.ui-tabs .ui-tabs-panel{border:1px solid #b9d8d9}.ui-tabs.ui-widget-content{background:transparent none;border:0}.ui-tabs .ui-state-default a,.ui-tabs .ui-state-default a:link,.ui-tabs .ui-state-default a:visited{color:#004d99}.ui-tabs .ui-state-hover a,.ui-tabs .ui-state-hover a:link,.ui-tabs .ui-state-hover a:visited{color:#538200}.ui-widget,.ui-widget button,.ui-widget input,.ui-widget select,.ui-widget textarea{font-family:inherit;font-size:inherit}.statictabs ul{background:none repeat scroll 0 0 transparent;border:0 none;border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-top-left-radius:4px;border-top-right-radius:4px;color:#222;font-size:100%;font-weight:700;line-height:1.3;list-style:none outside none;margin:0;outline:0 none;padding:.2em .2em 0;text-decoration:none}.statictabs ul:after{clear:both}.statictabs ul:after,.statictabs ul:before{content:"";display:table}.statictabs ul li{background:none repeat scroll 0 0 #e6f0f2;border:1px solid #b9d8d9;border-bottom:0 none;border-top-left-radius:4px;border-top-right-radius:4px;color:#555;float:left;font-weight:400;list-style:none outside none;margin-bottom:0;margin-right:.4em;padding:0;position:relative;top:1px;white-space:nowrap}.statictabs ul li.active{background-color:#fff;color:#212121;font-weight:400;padding-bottom:1px}.statictabs ul li.active a{background:none repeat scroll 0 0 transparent;color:#000;cursor:text;font-weight:700;outline:0 none;top:1px}.statictabs ul li a{color:#004d99;cursor:pointer;float:left;padding:.5em 1em;text-decoration:none}.statictabs ul li a:hover{background-color:#edf4f5;border-top-left-radius:4px;border-top-right-radius:4px;color:#538200}.statictabs .tabs-container{background:none repeat scroll 0 0 transparent;border:1px solid #b9d8d9;border-bottom-left-radius:4px;border-bottom-right-radius:4px;color:#222;display:block;padding:1em 1.4em}.authref{font-style:normal;text-indent:4em}.seealso,.seefrom{font-style:italic;text-indent:2em}#authfinderops{float:right}.authorizedheading{font-weight:700}.authres_notes,.authres_otherscript,.authres_seealso{padding-top:3px}.authres_notes{font-style:italic}.contents{width:75%}.contents .r,.contents .t{display:inline}.contents .t{font-weight:700}.contents .t:first-child:before{content:"â "}.contents .t:before{content:"\Aâ ";white-space:pre}.contentblock{margin-left:2em;position:relative}#hierarchies a{color:#069;font-weight:400;text-decoration:underline}#hierarchies a:hover{color:#903}#didyoumeanintranet,#didyoumeanopac{float:left;width:260px}.pluginlist{padding-bottom:10px}.plugin{margin:0 1em 1em 0}.pluginname{background-color:#e6f0f2;cursor:move;margin:.3em;padding-bottom:4px;padding-left:.2em}.pluginname .ui-icon{float:right}.plugindesc{padding:.4em}.ui-sortable-placeholder{border:1px dotted #000;height:80px;visibility:visible}.ui-sortable-placeholder *{visibility:hidden}.ui-datepicker{box-shadow:1px 1px 3px 0 #666}.ui-datepicker table{border:0;border-collapse:collapse;font-size:.9em;margin:0 0 .4em;width:100%}.ui-datepicker th{background:transparent none;border:0;font-weight:700;padding:.7em .3em;text-align:center}.ui-datepicker-trigger{margin:0 3px;vertical-align:middle}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dd{margin:0 10px 10px 65px}.ui-timepicker-div dl dt{height:25px;margin-bottom:-25px}.ui-timepicker-div dl td{font-size:90%}.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-tpicker-grid-label{background:none;border:0;margin:0;padding:0}.ui_tpicker_microsec,.ui_tpicker_millisec,.ui_tpicker_second{display:none}.ui-accordion-header,.ui-widget-content .ui-accordion-header{font-size:110%;font-weight:700}video{width:480px}.btn,button{border-color:#adadad #adadad #949494;font-family:Arial,Verdana,Helvetica,sans-serif}.btn.btn-link,button.btn-link{border:0}.btn-group-xs>.btn,.btn-xs{font-size:10.5px;padding:3px 5px}#toolbar .dropdown-menu{border-top-width:1px;font-size:13px}#toolbar.floating{border-radius:0;margin-top:0}.dropdown-menu{border-color:rgba(0,0,0,.2);border-top:0;font-size:12px}.dropdown-menu li{list-style:none outside none}.dropdown-menu li>a{padding:4px 20px}.dropdown-menu li>a:focus,.dropdown-menu li>a:hover{background-image:linear-gradient(180deg,#08c,#0077b3);background-repeat:repeat-x;color:#fff;text-decoration:none}.navbar{color:#333;min-height:20px}.navbar .nav>li{list-style:none outside none;padding:0 .6em}.navbar .nav>li>a{color:#004d99;font-weight:700;padding:.4em .2em}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#538200}.navbar .nav li .dropdown.active>.dropdown-toggle:focus,.navbar .nav li .dropdown.open.active>.dropdown-toggle:focus,.navbar .nav li .dropdown.open>.dropdown-toggle:focus{background:#e6f0f2 none;box-shadow:none}#changelanguage .dropdown-menu>li>a,#changelanguage .dropdown-menu>li>span{padding:5px 15px}#changelanguage .navbar-text{margin:0}#changelanguage .navbar-text span{display:block;line-height:20px}.loggedout{color:#004d99;font-weight:700;padding:.4em .2em}.navbar-static-top .navbar-inner{background:#e6f0f2 none;border:0;box-shadow:none;min-height:0;padding-left:0}.navbar-fixed-bottom .navbar-inner{min-height:0;padding:.4em 0}.navbar-fixed-bottom .nav>li{border-right:1px solid #ccc}.navbar-fixed-bottom .nav>li>a{font-weight:400}.navbar-fixed-bottom .nav>li:last-child{border-right:0}.navbar-fixed-bottom .nav>li.navbar-text{line-height:normal;padding:.4em .7em}.tooltip.bottom .tooltip-arrow{border-bottom-color:#eee}.tooltip.bottom .tooltip-inner{background-color:#fff;border:1px solid rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);color:#000;font-size:120%;padding:1em}.separator{color:#666;padding:0 .2em}.close{-webkit-filter:none;filter:none;float:none;font-weight:400;line-height:1.5;position:inherit;right:auto;text-shadow:none;top:auto}.close,.close:hover{font-size:inherit;opacity:inherit}.close:hover{color:inherit;-webkit-filter:inherit;filter:inherit}.checkbox label,.radio label{margin-left:20px;padding-left:0}.checkbox input[type=checkbox],.radio input[type=radio]{margin-left:0;position:relative}.modal-header .closebtn{margin-top:4px}.closebtn{color:#000;filter:alpha(opacity=20);float:right;font-size:21px;font-weight:700;line-height:1;opacity:.2;text-shadow:0 1px 0 #fff}.closebtn:focus,.closebtn:hover{color:#000;cursor:pointer;filter:alpha(opacity=50);opacity:.5;text-decoration:none}.modal-body{background-color:#fff;overflow-y:auto}.modal-content{background-color:#edf4f6}.btn-group label,.btn-group select{font-size:13px}.tooltip-inner{white-space:pre-wrap}pre{border:0;border-radius:0;display:block;line-height:inherit;margin:0;word-break:break-all;word-wrap:break-word}code,pre{background-color:transparent;color:inherit;font-size:inherit;padding:0}code{border-radius:0}.pagination>li>a,.pagination>li>span{font-weight:700}.waiting{cursor:wait}#jobfailed,#jobpanel,#jobstatus{display:none}#jobstatus{margin:.4em}#jobprogress{background:url(../img/progress.png) -300px 0 no-repeat;border:1px solid #666;display:inline-block;height:10px;width:200px}.progress_panel{border:2px solid #eee;border-radius:5px;clear:both;font-size:120%;margin:1em 0;padding:1em}progress{width:50%}#selections{white-space:normal;width:100%}#selections input{margin:0 2px;vertical-align:middle}#selections span{background-color:#ebf3ff;border-radius:5px;font-size:75%;line-height:240%;margin:3px;padding:3px;white-space:nowrap}#selections span.selected{background-color:#cce0fc}#changepasswordf input[type=password],#changepasswordf input[type=text]{font-family:Courier New,Courier,monospace;font-size:140%;padding:.3em}.floating{box-shadow:0 3px 2px 0 rgba(0,0,0,.5)}.inline{display:inline}.nowrap,.tag_editor{white-space:nowrap}.tag_editor{background:transparent url(../img/edit-tag.png) 0 0 no-repeat;display:block;float:left;height:16px;margin:4px;overflow:hidden;text-indent:100%;width:16px}.browse-controls{margin-left:1.1em;margin-right:.5em;padding-bottom:1em;padding-top:1em}#browse-return-to-results{border-top-left-radius:3px;border-top-right-radius:3px;display:block;text-align:center}.browse-button{color:#004d99;display:inline-block;padding:.4em .6em}.browse-button:hover{background:#fafafa}span.browse-button{background:#fafafa;color:#222}span.circ-hlt{color:#c00;font-weight:700}span.expired{color:#900;font-style:italic}span.name{font-style:italic;font-weight:700}span.permissiondesc{font-weight:400}span.required{color:#c00;font-style:italic;margin-left:.5em}.result-biblio-itemtype{float:right;font-size:85%;margin:.5em;padding:.5em;text-align:center}.result-biblio-itemtype img{display:block;margin:auto;margin-bottom:2px}.browse-label,.browse-prev-next{border:1px solid #b9d8d9}.browse-label{background-color:#e8f0f6;border-top-left-radius:5px;border-top-right-radius:5px}.browse-prev-next{border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top-width:0}#browse-previous{border-bottom-left-radius:5px;border-right:1px solid #b9d8d9;padding-right:1em}#browse-next{border-bottom-right-radius:5px;border-top-width:0;float:right;padding-right:1em}.loading-overlay{background-color:#fff;cursor:wait;height:100%;left:0;opacity:.7;position:fixed;top:0;width:100%;z-index:3}.loading-overlay div{background:transparent url(../img/loading.gif) 0 0 no-repeat;font-size:175%;font-weight:700;height:2em;left:50%;margin:-1em 0 0 -2.5em;padding-left:50px;position:absolute;top:50%;width:15em}#merge_invoices{display:none;margin:1em auto}#merge{margin:.5em 0 0}#merge_table tr.active td{background-color:#ffc}.renewals{display:block;font-size:.8em;padding:.5em}#transport-types{padding-top:.5px}#i18nMenu .navbar-text .currentlanguage{color:#000;font-weight:700}#i18nMenu a.currentlanguage:link,#i18nMenu a.currentlanguage:visited{font-weight:700}#i18nMenu a .sublanguage-selected{color:#000;font-weight:700}#circ_circulation_issue .onsite_checkout-select,.onsite_checkout-select label{font-size:inherit;font-weight:400}.onsite_checkout{color:#c00}.onsite-checkout-only{background-color:rgba(255,242,206,.5);border:1px solid #fff2ce;border-radius:4px}.branchgriditem{background-color:#fff;border:1px solid #b9d8d9;border-radius:3px;display:table-cell;float:left;margin:3px;padding:.3em}.branchgridrow{display:table-row}.branchselector{display:table}.hq-author{font-weight:700}#cn_browser_table_wrapper>#cn_browser_table{margin:auto;width:90%}#new_rule{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;display:none;margin:.3em;padding:.3em}.blocks{margin-bottom:.3em}.remove_rule{font-size:80%;padding-left:.7em}.underline{text-decoration:underline}.overline{text-decoration:overline}.order-control{padding-right:5px}#borrower_message{margin-top:10px}.form-group{margin-bottom:10px}.form-group label{font-weight:700}.modal-textarea{width:98%}#pat_member #patron_list_dialog,#pat_member #searchresults,#patron_search #filters{display:none}#fixedlengthbuilderaction{border:3px solid #e6f0f2;left:80%;padding:5px;position:relative;top:-80px;width:12%}.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background:#e6f0f2 none;box-shadow:none}.navbar-default.navbar-fixed-bottom .navbar-nav>.open>a:focus,.navbar-default.navbar-fixed-bottom .navbar-nav>.open>a:hover{background:transparent none;box-shadow:none}#interlibraryloans #dataPreviewLabel{margin:.3em 0}#interlibraryloans h1{margin:1em 0}#interlibraryloans h2{margin-bottom:20px}#interlibraryloans h3{margin-top:20px}#interlibraryloans .bg-info{overflow:auto;position:relative}#interlibraryloans .format h4{margin-bottom:20px}#interlibraryloans .format h5{margin-top:20px}#interlibraryloans .format input{margin:10px 0}#interlibraryloans .format li{list-style:none}#interlibraryloans #add-new-fields{margin:1em}#interlibraryloans #column-toggle,#interlibraryloans #reset-toggle{font-weight:700;line-height:1.5em;margin:15px 0}#interlibraryloans #freeform-fields .custom-name{margin-right:1em;text-align:right;width:9em}#interlibraryloans #freeform-fields .delete-new-field{margin-left:1em}#interlibraryloans #search-summary{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}#ill-view-panel{margin-top:15px}#ill-view-panel h3{margin-bottom:10px}#ill-view-panel h4{margin-bottom:20px}#ill-view-panel .notesopac{display:inline-block}#ill-view-panel .rows div{height:1em;margin-bottom:1em}#requestattributes{font-family:monospace;line-height:1.3em}#ill-requests{width:100%!important}#helper span,#logged-in-info-full{display:none}.loggedin-menu-label{color:#777;font-size:12px;line-height:1.42857143;padding:4px 12px;white-space:nowrap}.loggedin-menu-label span{color:#000;font-weight:700}.loggedin-menu-label.divider{padding:0}.buttons-list{margin-bottom:30px;padding:0}.buttons-list li{list-style-type:none}.buttons-list li a.circ-button{background-color:#f4f8f9;background-position:5px 3px;background-repeat:no-repeat;border:2px solid #b9d8d9;border-radius:6px;box-sizing:content-box;color:#000;display:block;font-size:110%;font-weight:700;margin:.5em 0;max-width:260px;padding:8px;text-decoration:none}.buttons-list li a.circ-button:hover{border-color:#538200;color:#538200}@media (min-width:200px){.navbar-nav>li{float:left}.navbar-right{float:right!important;margin-right:-15px}.navbar-nav{float:left;margin:0}.navbar-nav .open .dropdown-menu{background-color:#fff;border:1px solid rgba(0,0,0,.15);box-shadow:0 6px 12px rgba(0,0,0,.175);float:left;position:absolute;width:auto}.navbar-nav .open .dropdown-menu.dropdown-menu-left{left:auto;right:0}.navbar-nav .open .dropdown-menu.dropdown-menu-right{right:auto}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{background-color:#0081c2;background-image:linear-gradient(180deg,#08c,#0077b3);background-repeat:repeat-x;color:#fff;text-decoration:none}}@media (min-width:800px){#helper i{display:none}#helper span,#logged-in-info-full{display:inline}#logged-in-info-brief,.loggedin-menu-label{display:none}} >\ No newline at end of file >+@charset "UTF-8";@import url("../../lib/yui/reset-fonts-grids.css") screen;::-moz-selection{background:#538200;color:#fff}::selection{background:#538200;color:#fff}a:link,a:visited{color:#004d99;text-decoration:none}a:active,a:hover{color:#538200;text-decoration:none}a:hover .term{color:#ff9090}a.btn:link,a.btn:visited{color:#333}a.btn.btn-link:link,a.btn.btn-link:visited{color:#004d99}a.btn.btn-link:hover{color:#538200}a.cancel{padding-left:1em}a.cartRemove{color:#c33;font-size:90%;margin:0;padding:0}a.close:hover{color:#538200}a.csv{background-image:url(../img/famfamfam/silk/page_white_excel.png)}a.dropdown-toggle{white-space:nowrap}a.incart{color:#666}a.debit,a.overdue{color:#c00}a.popup{background:transparent url(../img/pop-up-link.png) 100% no-repeat;padding-right:15px}a.disabled{color:#999}a.document{background-position:0 middle;background-repeat:no-repeat;display:inline-block;min-height:20px;padding-left:20px}a.highlight_toggle{display:none}a .localimage img{border:1px solid #00c;margin:0 .5em;padding:.3em}a.pdf{background-image:url(../img/famfamfam/silk/page_white_acrobat.png)}a.submit{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em;display:inline-block}a.submit:active{border:1px inset #999}a.submit:disabled{background:#eee none;border:1px solid silver;color:#999}a.term{text-decoration:underline}a.xml{background-image:url(../img/famfamfam/silk/page_white_code.png)}aside h5{font-size:100%;margin:.5em 0}aside fieldset.brief{margin:0;padding:.4em .7em}aside fieldset.brief fieldset{margin:0;padding:.5em 0}aside fieldset.brief fieldset legend{font-size:85%}aside fieldset.brief li.checkbox label,aside fieldset.brief li.dateinsert label,aside fieldset.brief li.dateinsert span.label{display:inline}aside fieldset.brief li.radio{padding:.7em 0}aside fieldset.brief li.radio input{padding:.3em 0}aside fieldset.brief li.radio label,aside fieldset.brief li.radio span.label{display:inline}aside fieldset.brief ol{font-size:85%;margin:0;padding:0}aside fieldset.brief [type=text],aside fieldset.brief select{width:100%}button{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}button:active{border:1px inset #999}button:disabled{background:#eee none;border:1px solid silver;color:#999}button.closebtn{background:transparent;border:0;cursor:pointer;padding:0}main .yui-b fieldset.brief [type=text],main .yui-b fieldset.brief select{width:auto}table{border-collapse:collapse;border-right:1px solid #bcbcbc;border-top:1px solid #bcbcbc}table .btn-group{white-space:nowrap}table .btn-group .btn{display:inline-block;float:none}table.indexes td{vertical-align:middle}table>caption span.actions{font-size:66%;font-weight:400;margin:0 .5em 0 0}table.invis,table.invis td,table.invis tr{border:0}table+table{margin-top:1em}td,th{border-bottom:1px solid #bcbcbc;border-left:1px solid #bcbcbc;padding:.2em .3em}td{background-color:#fff;vertical-align:top}td.actions{white-space:nowrap}td.borderless{border:0 none;border-collapse:separate}td.data{font-family:Courier New,Courier,monospace}td.data,td.total{text-align:right}td input.approve{background-color:#ffc}th{background-color:#e8e8e8;font-weight:700;text-align:center}th.data{font-family:Courier New,Courier,monospace;text-align:right}body{font-family:Arial,Verdana,Helvetica,sans-serif;padding:0 0 4em;text-align:left}br.clear{clear:both;line-height:1px}form{display:inline}form.confirm{display:block;text-align:center}h1{font-size:161.6%;font-weight:700}h1#logo{border:0 none;float:left;margin:.75em .3em .75em .7em;padding:0;width:180px}h2{font-size:146.5%}h2,h3{font-weight:700}h3{font-size:131%}h4{font-size:116%}h4,h5{font-weight:700}h5{font-size:100%}h6{font-size:93%;font-weight:700}h1,h2,h3,h4,h5,h6{margin:.3em 0}hr{clear:both}p{margin:.5em 0}strong{font-weight:700}em strong,strong em{font-style:italic;font-weight:700}cite,em{font-style:italic}input,textarea{line-height:normal;padding:2px 4px}input:focus,textarea:focus{border-color:#538200;border-radius:4px;border-style:solid}input[type=checkbox],input[type=radio]{margin:0;vertical-align:middle}input[type=button]:active,input[type=submit]:active{border:1px inset #999}input[type=button],input[type=reset],input[type=submit]{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}input[type=button]:active,input[type=reset]:active,input[type=submit]:active{border:1px inset #999}input[type=button]:disabled,input[type=reset]:disabled,input[type=submit]:disabled{background:#eee none;border:1px solid silver;color:#999}input.alert{background-color:#ff9;border-color:#900}input.submit{background:linear-gradient(180deg,#fff 0,#f7f7f7 35%,#e0e0e0);border:1px outset #999;border-left-color:#666;border-top-color:#666;color:#333;padding:.25em}input.submit:active{border:1px inset #999}input.submit:disabled{background:#eee none;border:1px solid silver;color:#999}input.warning{background:#fff url(../img/famfamfam/silk/error.png) no-repeat 4px;padding:.25em .25em .25em 25px}.label,label{color:#000;display:inline;font-size:inherit;font-weight:400;max-width:inherit;padding:0;vertical-align:middle}.label input[type=checkbox],.label input[type=radio],label input[type=checkbox],label input[type=radio]{margin-top:0}.label.circ_barcode,label.circ_barcode{font-size:105%;font-weight:700}.label.permissioncode,label.permissioncode{font-style:italic}.label.permissioncode:before,label.permissioncode:before{content:"("}.label.permissioncode:after,label.permissioncode:after{content:")"}.label.required,label.required{color:#c00}.subfield-label{font-style:italic}.subfield-label span.subfield-code{font-weight:700}.members-update-table{padding-top:10px}#navmenulist li{border-bottom:1px solid #eee;list-style-image:url(../img/arrow-bullet.gif);padding:.2em 0}#navmenulist li a{text-decoration:none}#navmenulist li a.current{font-weight:700}#doc,#doc1,#doc2,#doc3{padding-top:1em}.main{margin-top:1em}#login_controls{padding:.4em .5em;position:absolute;right:.5em}ul{padding-left:1.1em}ul li{list-style-type:disc}ul li input.submit{font-size:87%;padding:2px}ul li li{list-style-type:circle}ul .toolbar{padding-left:0}ul .toolbar button{font-family:Arial,Verdana,Helvetica,sans-serif;padding-bottom:2px}ul .toolbar li{display:inline;list-style:none}ul.budget_hierarchy{margin-left:0;padding-left:0}ul.budget_hierarchy li{display:inline}ul.budget_hierarchy li:after{content:" -> "}ul.budget_hierarchy li:first-child:after,ul.budget_hierarchy li:last-child:after{content:""}ul.fa-ul li{list-style-type:none}ul.ui-tabs-nav li{list-style:none}ol{padding-left:1.5em}ol li{list-style:decimal}ol.bibliodetails{float:left;margin:0 0 1em 1em}ol.bibliodetails li{border-bottom:1px solid #e8e8e8;list-style-type:none;padding:.1em}ol.bibliodetails span.label{border-right:1px solid #e8e8e8;float:left;font-weight:700;margin-right:1em;width:12em}.gradient{background-image:linear-gradient(180deg,#e6f0f2 1%,#fff 99%);display:inline-block;width:100%}.cart-controls{border-top:1px solid #e8e8e8;padding:7px 0}#editions table,#editions td{border:0}.highlighted-row,.highlighted-row td{background-color:#ffd000!important}tbody tr:nth-child(odd) td{background-color:#f3f3f3;border:1px solid #bcbcbc;border-right:1px solid #bcbcbc}.overdue td.od{color:#c00;font-weight:700}tr.clickable{cursor:pointer}tr.expired td{color:#999}tr.highlight td{background-color:#f6f6f6;border-color:#bcbcbc}tr.highlight th[scope=row]{background-color:#ddd;border-color:#bcbcbc}tr.highlight table.invis td{border:0}tr.odd.onissue td{background-color:#ffffe1}tr.ok:nth-child(2n) td,tr.ok:nth-child(odd) td,tr.ok td,tr.onissue td{background-color:#ffc}tr.reserved td{background-color:#eeffd4}tr.transfered td{background-color:#e8f0f6}tr.warn:nth-child(odd) td,tr.warn td{background-color:#ff9090}.table_borrowers tr:hover td{background-color:#ff9}tfoot td{background-color:#f3f3f3;font-weight:700}caption{color:#000;font-size:133.9%;font-weight:700;margin:.3em 0}.problem{background-color:#ffc;color:#900;font-weight:700;line-height:1.7em}fieldset{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;margin:1em 1em 1em 0;padding:1em}fieldset+fieldset.action{padding-top:20px}fieldset .lastchecked{border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom-width:0;margin-bottom:0}fieldset.action{background-color:transparent;border:0;clear:both;float:none;margin:.9em 0 0;padding:.4em;width:auto}fieldset.brief{border:2px solid #b9d8d9}fieldset.brief div.hint{margin-bottom:.4em}fieldset.brief label{display:block;font-weight:700;padding:.3em 0}fieldset.brief label.inline{display:inline;float:none;margin-left:1em;width:auto}fieldset.brief li[aria-disabled=true]{color:#999}fieldset.brief li.inline{display:inline;float:none;margin-left:1em;width:auto}fieldset.brief li,fieldset.brief ol{list-style-type:none}fieldset.brief span .label{display:block;font-weight:700;padding:.3em 0;text-align:left}fieldset.rows{border:2px solid #b9d8d9;border-width:1px;clear:left;float:left;font-size:90%;margin:.9em 0 0;padding:0;width:100%}fieldset.rows fieldset{background-color:transparent;border-width:1px;margin:1em;padding:.3em}fieldset.rows fieldset.action{padding:1em}fieldset.rows.inputnote{clear:left;float:left;margin:1em 0 0 11em}fieldset.rows.left label{text-align:left;width:8em}fieldset.rows.left li{padding-bottom:.4em}fieldset.rows.left span label{text-align:left;width:8em}fieldset.rows.ui-accordion-content{border-top-left-radius:0;border-top-right-radius:0;margin:0;padding:0;width:auto}fieldset.rows.ui-accordion-content table{margin:0}fieldset.rows.unselected{background-color:#fff;border:0;border-width:0}fieldset.rows caption{font-size:120%}fieldset.rows div.hint{margin-bottom:.4em;margin-left:10.5em}fieldset.rows label{float:left;font-weight:700;margin-right:1em;text-align:right;width:9em}fieldset.rows label.error{float:none;margin-left:1em;width:auto}fieldset.rows label.inline{display:inline;float:none;margin-left:1em}fieldset.rows label.yesno{float:none;width:auto}fieldset.rows legend{font-size:110%;font-weight:700;margin-left:1em}fieldset.rows li{clear:left;float:left;list-style-type:none;padding-bottom:1em;width:100%}fieldset.rows li[aria-disabled=true]{color:#999}fieldset.rows li.radio{padding-left:9em;width:auto}fieldset.rows li.radio input+label{margin-left:0;padding-left:0}fieldset.rows li.radio label{float:none;margin:0 0 0 1em;width:auto}fieldset.rows li input+label{margin-left:0;padding-left:0}fieldset.rows ol{list-style-type:none;padding:1em 1em 0}fieldset.rows ol.radio label{float:none;margin-left:20px;margin-right:30px;padding-left:0;vertical-align:middle;width:auto}fieldset.rows ol.radio label.radio{float:left;margin-right:1em;margin-top:0;width:9em}fieldset.rows ol.radio input[type=checkbox],fieldset.rows ol.radio input[type=radio]{margin-left:-20px}fieldset.rows p{margin:1em 0 1em 1em}fieldset.rows span.label{float:left;font-weight:700;margin-right:1em;text-align:right;width:9em}fieldset.rows table{clear:both;font-size:105%;margin:1em 0 1em 1em}fieldset.rows table.mceListBox{margin:0}fieldset.rows td label{float:none;font-weight:400;width:auto}fieldset.rows .inputnote{clear:left;float:left;margin:1em 0 0 11em}fieldset.rows+h3{clear:both;padding-top:.5em}#multi_receiving fieldset.rows label{width:50%}.yui-u div.hint{margin-bottom:.4em}.yui-u fieldset.rows div.hint{margin-left:7.5em}.yui-u fieldset.rows label,.yui-u fieldset.rows span.label{width:10em}.yui-u .rows li p label.widelabel,legend{width:auto}legend{background-color:#fff;border:2px solid #b9d8d9;border-radius:3px;font-size:123.1%;font-weight:700;padding:.2em .5em}details>summary{cursor:pointer}details>summary:before{content:"\f0da";display:inline-block;font-family:FontAwesome;width:1em}details>summary.checkouts-by-itemtype li{display:inline-block}details[open]>summary:before{content:"\f0d7"}#floating-save{background-color:rgba(185,216,217,.6);bottom:3%;position:fixed;right:1%;width:150px}#breadcrumbs{background-color:#e6f0f2;clear:both;font-size:90%;margin:0;padding:.2em .5em .4em 10px}#header.navbar{margin-bottom:0}#header.navbar-default{background:#e6f0f2;border:0;box-shadow:none}#header+#breadcrumbs{margin-top:1em}#header>.container-fluid{padding:0}div.action{background-color:transparent;border:0;clear:both;float:none;margin:.9em 0 0;padding:.4em;width:auto}div .circmessage{margin-bottom:.3em;padding:0 .4em .4em}div .circmessage:first-child{margin-top:1em}div.error{background-color:#ff9;border:2px dashed #900;margin:1em;padding:.5em}div.first fieldset{margin-right:0}div.help{margin:.9em 0 0}div.justify{text-align:justify}div.message{background:linear-gradient(180deg,#fff 0,#f4f6fa 2%,#eaeef5 23%,#e8edf6 94%,#cddbf2);border:1px solid #bcbcbc;text-align:center;width:55%}div.message h5,div.message ul{padding-left:25%;text-align:left}div.message ul+h4{margin-top:.7em}div.note{background:linear-gradient(180deg,#f4f6fa 0,#e8edf6);border:1px solid #bcbcbc;margin:.5em 0;padding:.5em}div.note i.fa-exclamation{color:#c00;font-style:italic;padding:0 .3em}div.rules{display:block}div.results,div[class$=_table_controls]{padding:.7em 0}div.rule{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;margin:.3em;padding:.3em}div.lastchecked{border:2px solid #bcdb89;border-bottom-left-radius:5px;border-bottom-right-radius:5px;padding:.2em 1em}div.listgroup{clear:left}div.listgroup h4{font-style:italic}div.listgroup h4 a,div.listgroup input{font-size:80%}div.sysprefs h3{margin:.2em 0 .2em .4em}div.sysprefs dl{margin-left:1.5em}div.sysprefs.hint{float:right;margin:.7em;padding:.5em;width:25%}div.rows{clear:left;float:left;margin:0;padding:0;width:100%}div.rows+div.rows{margin-top:.6em}div.rows li{border-bottom:1px solid #eee;clear:left;float:left;list-style-type:none;padding-bottom:.2em;padding-top:.1em;width:100%}div.rows ol{list-style-type:none;padding:.5em 1em 0 0}div.rows ol li li{border-bottom:0}div.rows span.label{float:left;font-weight:700;margin-right:1em;padding-top:0;text-align:left;width:9em}div.pages{margin:.5em 0}div.pages a{font-weight:700;padding:1px 5px;text-decoration:none}div.pages a:link,div.pages a:visited{background-color:#eee;color:#36c}div.pages a:active,div.pages a:hover{background-color:#ffc}div.pages .current,div.pages .currentPage{background-color:#e6fcb7;color:#666;font-weight:700;padding:1px 5px}div.pages .inactive{background-color:#f3f3f3;color:#bcbcbc;font-weight:700;padding:1px 5px}div .browse{margin:.5em 0}#header_search{background-position:.5em .5em;background-repeat:no-repeat;float:left;margin:.3em 0 .5em}#header_search input{font-size:1.3em}#header_search input.submit{font-size:1em}#header_search div.residentsearch{border:0;border-bottom:1px solid #85ca11;padding:0 0 .2em}#header_search ul.ui-tabs-nav{margin-left:1em;padding-top:0}#header_search ul.ui-tabs-nav li.ui-state-default{background:transparent none;border:0;top:0}#header_search ul.ui-tabs-nav li.ui-state-default a{padding:.3em .6em}#header_search ul.ui-tabs-nav li.ui-tabs-active{background-color:#fffff1;border:1px solid #85ca11;border-top-width:0;top:-2px}#header_search ul.ui-tabs-nav li.ui-tabs-active a{text-decoration:none}#header_search .ui-corner-top{border-radius:0 0 4px 4px}#header_search>div,#header_search>div>li,#header_search>ul,#header_search>ul>li{display:none}#header_search>div:first-of-type,#header_search>div>li:first-of-type,#header_search>ul:first-of-type,#header_search>ul>li:first-of-type{display:block}.head-searchbox{width:30em}#checkouts,#reserves{border:1px solid #b9d8d9;padding:1em}.tip{color:gray;font-size:93%}.single-line{white-space:nowrap}.ex{font-family:Courier New,Courier,monospace}.ex,dt{font-weight:700}dd{font-size:90%;font-weight:400;padding:.2em;text-indent:2.5em}#toolbar,.btn-toolbar{background-color:#edf4f6;border:1px solid #e6f0f2;border-radius:5px 5px 0 0;margin:0;padding:5px}#disabled2 a,#disabled a,#disabled a:hover{color:#999}.patroninfo{margin-top:-.5em}.patroninfo h5{border-right:1px solid #b9d8d9;margin-bottom:0;padding-bottom:.5em;padding-left:-.5em;padding-top:.3em}.patroninfo h5:empty{border-right:0}.patroninfo ul{border:0;border-bottom:0;border-right:1px solid #b9d8d9;border-top:0;margin:0;padding:0}.patroninfo ul li{list-style-type:none;margin:0}.patroninfo+#menu{margin-right:0}#patronbasics div{background:transparent url(../img/patron-blank.min.svg) 10px 5px no-repeat;height:125px;padding:0;width:105px}#patronbasics div,#patronimage{border:1px solid #ccc;margin:.3em 0 .3em .3em}#patronimage{max-width:140px;padding:.2em}.patronviews{border-right:1px solid #000;border-top:1px solid #000;margin-bottom:.5em;padding:.5em 0}.column-tool{font-size:80%}.hint{color:#666;font-size:95%}.yui-b fieldset.brief{padding:.4em .7em}.yui-b fieldset.brief fieldset{margin:0 .3em;padding:.5em}.yui-b fieldset.brief fieldset legend{font-size:85%}#tools_holidays .yui-b fieldset.brief li.checkbox input{margin-left:0}.yui-b fieldset.brief li.checkbox label{display:inline}#tools_holidays .yui-b fieldset.brief li.checkbox label{margin-left:20px}.yui-b fieldset.brief li.dateinsert label,.yui-b fieldset.brief li.dateinsert span.label{display:inline}.yui-b fieldset.brief li.radio{padding:.7em 0}.yui-b fieldset.brief li.radio input{padding:.3em 0}#tools_holidays .yui-b fieldset.brief li.radio input{margin-left:0}.yui-b fieldset.brief li.radio label{display:inline}#tools_holidays .yui-b fieldset.brief li.radio label{margin-left:20px}.yui-b fieldset.brief li.radio label span.label{display:inline}.yui-b fieldset.brief ol{font-size:85%;margin:0;padding:0}.yui-b fieldset.brief [type=text],.yui-b fieldset.brief select{width:100%}.yui-b fieldset.rows div.hint{margin-left:10.5em}#yui-main .yui-b fieldset.brief [type=text],#yui-main .yui-b fieldset.brief select,.yui-b fieldset.rows td label,.yui-b fieldset.rows td span.label{width:auto}.btn-toolbar fieldset.action{margin-top:0}.btn-toolbar .dropdown-menu{font-size:13px}.rows .label{white-space:normal}.checkedout{color:#999;font-style:italic}.subfield_not_filled{background-color:#ff9}.content_hidden{display:none;visibility:hidden}.content_visible{display:block;visibility:visible}#z3950searcht table{border:0;padding:20px}#z3950_search_targets{height:338px;overflow-y:auto}#z3950_search_targets_acq{height:308px;overflow-y:auto}.z3950checks{padding-left:1em}.error{color:#c00}.status_ok{background-color:#90ee90}.status_warn{background-color:red}i.error{color:#c00}i.success{color:green}i.warn{color:orange}.checkout-setting{font-size:85%;padding-top:.3em}.checkout-setting input{vertical-align:middle}.checkout-setting label{font-size:inherit;font-weight:400}.checkout-settings{background-color:#f4f8f9;border-radius:0;border-top:2px solid #b9d8d9;display:none;margin-left:-1em;margin-right:-1em;margin-top:1em;padding:1em 1em 0}#show-checkout-settings{margin-top:.5em}.blocker,.inaccurate-item-statuses{color:#900}.circmessage li{list-style:url(../img/arrow-bullet.gif);margin-bottom:.2em}#circ_needsconfirmation{margin:auto}.dialog{border:1px solid #bcbcbc;border-radius:2px;margin:1em auto;padding:.5em;text-align:center;width:65%}.dialog a.approve{display:inline-block}.dialog a.approve,.dialog button{background:#fff none;border:1px outset #999;border-left-color:#666;border-top-color:#666;margin:.4em;padding:.4em;white-space:pre-line}.dialog a.approve:active,.dialog button:active{border:1px inset #999}.dialog a.approve:hover,.dialog button:hover{background-color:#ffc}.dialog h2,.dialog h3,.dialog h4{margin:auto;text-align:center}.dialog input{background-color:#fff;border:1px solid #bcbcbc;margin:.4em;padding:.4em .4em .4em 25px}.dialog input:hover{background-color:#ffc}.dialog input[type=submit]{background:#fff none}.dialog li{list-style-position:inside}.dialog table{margin:.5em auto}.dialog table td{text-align:left}.dialog table th{text-align:right}.alert{background:linear-gradient(180deg,#fef8d3 0,#ffec91 9%,#ffed87 89%,#f9dc00);border:1px solid #e0c726;color:inherit;text-align:center;text-shadow:none}.alert strong{color:#900}.alert .closebtn{line-height:20px;position:relative;right:-21px;top:-2px}.approve i.fa,.success i.fa{color:green}.deny i.fa{color:#c00}.new i.fa{color:#425faf}.accesskey{text-decoration:underline}.missing,.term{background-color:#ffc}.term{color:#900}.shelvingloc{display:block;font-style:italic}#menu{border-right:1px solid #b9d8d9;margin-right:.5em;padding-bottom:2em;padding-top:1em}#menu li a{background:linear-gradient(180deg,#e8f0f6 0,#e8f0f6 96%,#c1c1c1);border:1px solid #b9d8d9;border-bottom-left-radius:5px;border-top-left-radius:5px;display:block;font-size:111%;margin:.5em 0;margin-right:-1px;padding:.4em .3em;text-decoration:none}#menu li a:hover{background:linear-gradient(180deg,#fafafa 0,#fff 96%,#e6e6e6 97%,#ccc 99%,#c1c1c1)}#menu li.active a,#menu li a:hover{border-bottom:1px solid #85ca11;border-left:1px solid #85ca11;border-top:1px solid #85ca11}#menu li.active a{background-color:#fff;background-image:none;border-right:0;color:#000;font-weight:700}#menu li.active a:hover{background-color:#fff;color:#538200}#menu ul li{list-style-type:none}#logo{background:transparent url(../img/koha-logo-medium.png) no-repeat scroll 0;margin:.75em .3em .75em .7em}#logo a{border:0;cursor:pointer;display:block;height:0!important;margin:0;overflow:hidden;padding:44px 0 0;text-decoration:none;width:180px}#closewindow{margin-top:2em;text-align:center}#closewindow a{font-weight:700}.barcode{font-size:200%;vertical-align:middle}li.email{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.patronbriefinfo li.email{font-size:87%;padding:0 10px 0 0;width:90%}.empty{color:#ccc}.address{font-size:110%}.address li{list-style-type:none}.title{font-size:105%;font-weight:700}.hold{float:right;font-size:90%;margin:0}.thumbnail{display:block;margin:auto}.thumbnail>li{list-style-type:none}#searchresults ul li{clear:left;font-size:90%;list-style:url(../img/item-bullet.gif);padding:.2em 0}#searchresults ul li img{float:left;margin:3px 5px 3px -5px}#searchresults ul span.status{clear:left;color:#900;display:block}#searchresults ul span.unavailable{clear:left;display:block}#searchresults ul table td{vertical-align:top}#searchresults.unavailability strong{display:block}#searchheader{background-color:#e6f0f2;border:1px solid #b9d8d9;border-radius:5px 5px 0 0;font-size:80%;margin:0 0 .5em -1px;padding:.4em 0 .4em 1em}#searchheader.floating{border-radius:0;margin-top:0}#searchheader .btn-group>.btn:first-child{margin-left:.7em}#searchheader form{float:right;padding:5px 5px 3px 0}#searchheader form.fz3950{float:right;font-size:125%;padding:0 0 0 5em}#searchheader form.fz3950bigrpad{float:right;font-size:125%;padding:5px 25em 0 0}#search-facets{border:1px solid #b9d8d9;border-radius:5px 5px 0 0}#search-facets h4{background-color:#e6f0f2;border-bottom:1px solid #b9d8d9;border-radius:5px 5px 0 0;font-size:90%;margin:0;padding:.4em .2em;text-align:center}#search-facets ul{margin:0;padding:.3em}#search-facets ul li{font-weight:700;list-style-type:none}#search-facets li li{font-size:85%;font-weight:400;margin-bottom:2px;padding:.1em .2em}#search-facets li.showmore{font-weight:700;text-indent:1em}.facet-count{display:inline-block}#bookcoverimg{text-align:center}.searchhighlightblob{font-size:75%;font-style:italic}#displayexample{background-color:#ccc;margin-bottom:10px;padding:5px}#irregularity_summary{vertical-align:top}#toplevelmenu{padding:0}#CheckAll,#CheckNone,#CheckPending{font-weight:400;margin:0 .5em 0 0}.dmg,.lost,.wdn{color:#900;display:block}.datedue{color:#999;display:block;font-style:italic}.credit,.waitinghere{color:#690}#mainuserblock{border:1px solid #e8e8e8;margin-top:.5em;padding:.5em}.labeledmarc-table{border:0}.labeledmarc-label{border:0;color:#000;font-size:11pt;font-style:italic;padding:5}.labeledmarc-value{border:0;color:#000;font-size:10pt;padding:5}#marcPreview table{border:0;font-family:Courier New,Courier,monospace;font-size:95%;margin:.7em 0 0}#marcPreview tbody tr:nth-child(odd) td{background-color:#fff}#marcPreview td,#marcPreview th{border:0;padding:2px;vertical-align:top}#marcPreview th{background-color:#fff;text-align:left;white-space:nowrap}#marcPreview.modal-dialog,.modal-dialog.modal-wide{width:80%}@media (max-width:767px){#marcPreview{margin:0;width:auto}}#cartDetails{background-color:#fff;border:1px solid #739acf;box-shadow:1px 1px 3px 0 #666;color:#000;display:none;margin:0;padding:10px;text-align:center;width:180px;z-index:2}#cartmenulink{background:transparent url(../img/cart-small.gif) 0 no-repeat;padding-left:15px}#basketcount span{display:inline;font-size:90%;font-weight:400;padding:0}#moremenu{display:none}.results_summary{color:#707070;display:block;font-size:85%;padding:0 0 .5em}.results_summary a{font-weight:400}.results_summary .label{color:#202020}.child_fund_amount{font-style:italic}.number_box{font-size:105%;line-height:200%}.number_box a,.number_box span{background-color:#e4ecf5;border:1px solid #a4bedd;border-radius:4px;font-weight:700;padding:.1em .4em;text-decoration:none}.number_box a:hover,.number_box span:hover{background-color:#ebeff7}.container{border:1px solid #eee;margin:1em 0;padding:1em}.import_export{position:relative}.import_export .export_ok{background:#e3e3e3 none;border:0;cursor:pointer;margin-left:20px;padding:10px}.import_export .import_export_options{background:#fff;border:1px solid #cdcdcd;left:60px;padding:10px;position:absolute;top:0;width:300px;z-index:1}.import_export_options{background:#e3e3e3 none;border:0;cursor:pointer;margin-left:20px;padding:10px}.import_export_options fieldset.rows li label{width:16em}.import_export_options .importing{background:none;padding:inherit}.form_import fieldset.rows li label{width:auto}.form_import .input_import{border:1px solid #bcbcbc}.importing{position:relative}.importing .importing_msg{padding-bottom:10px;padding-left:10px}.field_hint{color:gray;font-style:italic;padding-left:1em}.m880{display:block;float:right;padding-left:20px;text-align:right;width:50%}.advsearch{margin:0}.advsearch table{border-collapse:separate;border-spacing:5px;border-width:0}.advsearch td{border:1px solid #eee;padding:.3em .4em}#circ_circulation_issue{position:relative}#clearscreen{position:absolute;right:0;top:0}#clearscreen a{background-color:#eee;border-radius:0 0 0 5px;color:#ccc;display:block;font-size:160%;font-weight:700;padding:0 .7em .2em;text-decoration:none;text-shadow:0 -1px 0 #666}#clearscreen a:hover{color:#c00}.pager{background-color:#e8e8e8;border:1px solid #bcbcbc;border-radius:5px;display:inline-block;font-size:85%;margin:.4em 0;padding:.3em .5em}.pager img{vertical-align:middle}.pager img.last{padding-right:5px}.pager input.pagedisplay{background-color:transparent;border:0;font-weight:700;text-align:center}.pager p{margin:0}.no-image{background-color:#fff;border:1px solid #aaa;border-radius:3px;color:#979797;display:block;font-size:86%;font-weight:700;text-align:center;width:75px}#acqui_order_supplierlist>div.supplier{border:1px solid #eee;margin:.5em;padding:1em}#acqui_order_supplierlist>div>div>.baskets{margin-top:.5em}#acqui_order_supplierlist>div>span.action{margin-left:5em}#acqui_order_supplierlist>div>span.suppliername{display:inline;font-size:1.7em;margin-bottom:.5em}.supplier-contact-details{float:left}#ADD-contact{margin:0 0 8px 8px}#contact-template{display:none}.ui-widget-content{background:#fff none;border:1px solid #b9d8d9;color:#222}.ui-widget-header{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#222;font-weight:700}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{background:#f4f8f9 none;border:1px solid #b9d8d9;color:#555;font-weight:400}.ui-state-focus,.ui-state-hover,.ui-widget-content .ui-state-focus,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-focus,.ui-widget-header .ui-state-hover{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#212121;font-weight:400}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{background:#fff none;border:1px solid #aaa;color:#212121;font-weight:400}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{background:#fff4c6;border:1px solid #fed22f;color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{background:#fef1ec;border:1px solid #cd0a0a;color:#cd0a0a}.ui-autocomplete{box-shadow:2px 2px 2px rgba(0,0,0,.3);cursor:default;position:absolute}.ui-autocomplete.ui-widget-content .ui-state-hover{background:#e6f0f2 none;border:1px solid #b9d8d9;color:#212121;font-weight:400}.ui-autocomplete-loading{background:#fff url(../img/spinner-small.gif) 100% no-repeat}.ui-menu li{list-style:none}.ui-tabs-nav .ui-tabs-active a,.ui-tabs-nav a:active,.ui-tabs-nav a:focus,.ui-tabs-nav a:hover,.ui-tabs-nav span.a{background:none repeat scroll 0 0 transparent;outline:0 none}.ui-tabs-nav.ui-widget-header{background:none;border:0}.ui-tabs .ui-tabs-nav li{background:#e6f0f2 none;border:1px solid #b9d8d9;margin-right:.4em;top:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active{background-color:#fff;border:1px solid #b9d8d9;border-bottom-width:0}.ui-tabs .ui-tabs-nav li.ui-tabs-active a{color:#000;font-weight:700}.ui-tabs .ui-tabs-nav li.ui-tabs-active.ui-state-hover{background:#fff none}.ui-tabs .ui-tabs-nav li.ui-state-default.ui-state-hover{background:#edf4f5 none}.ui-tabs .ui-tabs-panel{border:1px solid #b9d8d9}.ui-tabs.ui-widget-content{background:transparent none;border:0}.ui-tabs .ui-state-default a,.ui-tabs .ui-state-default a:link,.ui-tabs .ui-state-default a:visited{color:#004d99}.ui-tabs .ui-state-hover a,.ui-tabs .ui-state-hover a:link,.ui-tabs .ui-state-hover a:visited{color:#538200}.ui-widget,.ui-widget button,.ui-widget input,.ui-widget select,.ui-widget textarea{font-family:inherit;font-size:inherit}.statictabs ul{background:none repeat scroll 0 0 transparent;border:0 none;border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-top-left-radius:4px;border-top-right-radius:4px;color:#222;font-size:100%;font-weight:700;line-height:1.3;list-style:none outside none;margin:0;outline:0 none;padding:.2em .2em 0;text-decoration:none}.statictabs ul:after{clear:both}.statictabs ul:after,.statictabs ul:before{content:"";display:table}.statictabs ul li{background:none repeat scroll 0 0 #e6f0f2;border:1px solid #b9d8d9;border-bottom:0 none;border-top-left-radius:4px;border-top-right-radius:4px;color:#555;float:left;font-weight:400;list-style:none outside none;margin-bottom:0;margin-right:.4em;padding:0;position:relative;top:1px;white-space:nowrap}.statictabs ul li.active{background-color:#fff;color:#212121;font-weight:400;padding-bottom:1px}.statictabs ul li.active a{background:none repeat scroll 0 0 transparent;color:#000;cursor:text;font-weight:700;outline:0 none;top:1px}.statictabs ul li a{color:#004d99;cursor:pointer;float:left;padding:.5em 1em;text-decoration:none}.statictabs ul li a:hover{background-color:#edf4f5;border-top-left-radius:4px;border-top-right-radius:4px;color:#538200}.statictabs .tabs-container{background:none repeat scroll 0 0 transparent;border:1px solid #b9d8d9;border-bottom-left-radius:4px;border-bottom-right-radius:4px;color:#222;display:block;padding:1em 1.4em}.authref{font-style:normal;text-indent:4em}.seealso,.seefrom{font-style:italic;text-indent:2em}#authfinderops{float:right}.authorizedheading{font-weight:700}.authres_notes,.authres_otherscript,.authres_seealso{padding-top:3px}.authres_notes{font-style:italic}.contents{width:75%}.contents .r,.contents .t{display:inline}.contents .t{font-weight:700}.contents .t:first-child:before{content:"â "}.contents .t:before{content:"\Aâ ";white-space:pre}.contentblock{margin-left:2em;position:relative}#hierarchies a{color:#069;font-weight:400;text-decoration:underline}#hierarchies a:hover{color:#903}#didyoumeanintranet,#didyoumeanopac{float:left;width:260px}.pluginlist{padding-bottom:10px}.plugin{margin:0 1em 1em 0}.pluginname{background-color:#e6f0f2;cursor:move;margin:.3em;padding-bottom:4px;padding-left:.2em}.pluginname .ui-icon{float:right}.plugindesc{padding:.4em}.ui-sortable-placeholder{border:1px dotted #000;height:80px;visibility:visible}.ui-sortable-placeholder *{visibility:hidden}.ui-datepicker{box-shadow:1px 1px 3px 0 #666}.ui-datepicker table{border:0;border-collapse:collapse;font-size:.9em;margin:0 0 .4em;width:100%}.ui-datepicker th{background:transparent none;border:0;font-weight:700;padding:.7em .3em;text-align:center}.ui-datepicker-trigger{margin:0 3px;vertical-align:middle}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dd{margin:0 10px 10px 65px}.ui-timepicker-div dl dt{height:25px;margin-bottom:-25px}.ui-timepicker-div dl td{font-size:90%}.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-tpicker-grid-label{background:none;border:0;margin:0;padding:0}.ui_tpicker_microsec,.ui_tpicker_millisec,.ui_tpicker_second{display:none}.ui-accordion-header,.ui-widget-content .ui-accordion-header{font-size:110%;font-weight:700}video{width:480px}.btn,button{border-color:#adadad #adadad #949494;font-family:Arial,Verdana,Helvetica,sans-serif}.btn.btn-link,button.btn-link{border:0}.btn-group-xs>.btn,.btn-xs{font-size:10.5px;padding:3px 5px}#toolbar .dropdown-menu{border-top-width:1px;font-size:13px}#toolbar.floating{border-radius:0;margin-top:0}.dropdown-menu{border-color:rgba(0,0,0,.2);border-top:0;font-size:12px}.dropdown-menu li{list-style:none outside none}.dropdown-menu li>a{padding:4px 20px}.dropdown-menu li>a:focus,.dropdown-menu li>a:hover{background-image:linear-gradient(180deg,#08c,#0077b3);background-repeat:repeat-x;color:#fff;text-decoration:none}.navbar{color:#333;min-height:20px}.navbar .nav>li{list-style:none outside none;padding:0 .6em}.navbar .nav>li>a{color:#004d99;font-weight:700;padding:.4em .2em}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#538200}.navbar .nav li .dropdown.active>.dropdown-toggle:focus,.navbar .nav li .dropdown.open.active>.dropdown-toggle:focus,.navbar .nav li .dropdown.open>.dropdown-toggle:focus{background:#e6f0f2 none;box-shadow:none}#changelanguage .dropdown-menu>li>a,#changelanguage .dropdown-menu>li>span{padding:5px 15px}#changelanguage .navbar-text{margin:0}#changelanguage .navbar-text span{display:block;line-height:20px}.loggedout{color:#004d99;font-weight:700;padding:.4em .2em}.navbar-static-top .navbar-inner{background:#e6f0f2 none;border:0;box-shadow:none;min-height:0;padding-left:0}.navbar-fixed-bottom .navbar-inner{min-height:0;padding:.4em 0}.navbar-fixed-bottom .nav>li{border-right:1px solid #ccc}.navbar-fixed-bottom .nav>li>a{font-weight:400}.navbar-fixed-bottom .nav>li:last-child{border-right:0}.navbar-fixed-bottom .nav>li.navbar-text{line-height:normal;padding:.4em .7em}.tooltip.bottom .tooltip-arrow{border-bottom-color:#eee}.tooltip.bottom .tooltip-inner{background-color:#fff;border:1px solid rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);color:#000;font-size:120%;padding:1em}.separator{color:#666;padding:0 .2em}.close{-webkit-filter:none;filter:none;float:none;font-weight:400;line-height:1.5;position:inherit;right:auto;text-shadow:none;top:auto}.close,.close:hover{font-size:inherit;opacity:inherit}.close:hover{color:inherit;-webkit-filter:inherit;filter:inherit}.checkbox label,.radio label{margin-left:20px;padding-left:0}.checkbox input[type=checkbox],.radio input[type=radio]{margin-left:0;position:relative}.modal-header .closebtn{margin-top:4px}.closebtn{color:#000;filter:alpha(opacity=20);float:right;font-size:21px;font-weight:700;line-height:1;opacity:.2;text-shadow:0 1px 0 #fff}.closebtn:focus,.closebtn:hover{color:#000;cursor:pointer;filter:alpha(opacity=50);opacity:.5;text-decoration:none}.modal-body{background-color:#fff;overflow-y:auto}.modal-content{background-color:#edf4f6}.btn-group label,.btn-group select{font-size:13px}.tooltip-inner{white-space:pre-wrap}pre{border:0;border-radius:0;display:block;line-height:inherit;margin:0;word-break:break-all;word-wrap:break-word}code,pre{background-color:transparent;color:inherit;font-size:inherit;padding:0}code{border-radius:0}.pagination>li>a,.pagination>li>span{font-weight:700}.waiting{cursor:wait}#jobfailed,#jobpanel,#jobstatus{display:none}#jobstatus{margin:.4em}#jobprogress{background:url(../img/progress.png) -300px 0 no-repeat;border:1px solid #666;display:inline-block;height:10px;width:200px}.progress_panel{border:2px solid #eee;border-radius:5px;clear:both;font-size:120%;margin:1em 0;padding:1em}progress{width:50%}#selections{white-space:normal;width:100%}#selections input{margin:0 2px;vertical-align:middle}#selections span{background-color:#ebf3ff;border-radius:5px;font-size:75%;line-height:240%;margin:3px;padding:3px;white-space:nowrap}#selections span.selected{background-color:#cce0fc}#changepasswordf input[type=password],#changepasswordf input[type=text]{font-family:Courier New,Courier,monospace;font-size:140%;padding:.3em}.floating{box-shadow:0 3px 2px 0 rgba(0,0,0,.5)}.inline{display:inline}.nowrap,.tag_editor{white-space:nowrap}.tag_editor{background:transparent url(../img/edit-tag.png) 0 0 no-repeat;display:block;float:left;height:16px;margin:4px;overflow:hidden;text-indent:100%;width:16px}.browse-controls{margin-left:1.1em;margin-right:.5em;padding-bottom:1em;padding-top:1em}#browse-return-to-results{border-top-left-radius:3px;border-top-right-radius:3px;display:block;text-align:center}.browse-button{color:#004d99;display:inline-block;padding:.4em .6em}.browse-button:hover{background:#fafafa}span.browse-button{background:#fafafa;color:#222}span.circ-hlt{color:#c00;font-weight:700}span.expired{color:#900;font-style:italic}span.name{font-style:italic;font-weight:700}span.permissiondesc{font-weight:400}span.required{color:#c00;font-style:italic;margin-left:.5em}.result-biblio-itemtype{float:right;font-size:85%;margin:.5em;padding:.5em;text-align:center}.result-biblio-itemtype img{display:block;margin:auto;margin-bottom:2px}.browse-label,.browse-prev-next{border:1px solid #b9d8d9}.browse-label{background-color:#e8f0f6;border-top-left-radius:5px;border-top-right-radius:5px}.browse-prev-next{border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top-width:0}#browse-previous{border-bottom-left-radius:5px;border-right:1px solid #b9d8d9;padding-right:1em}#browse-next{border-bottom-right-radius:5px;border-top-width:0;float:right;padding-right:1em}.loading-overlay{background-color:#fff;cursor:wait;height:100%;left:0;opacity:.7;position:fixed;top:0;width:100%;z-index:3}.loading-overlay div{background:transparent url(../img/loading.gif) 0 0 no-repeat;font-size:175%;font-weight:700;height:2em;left:50%;margin:-1em 0 0 -2.5em;padding-left:50px;position:absolute;top:50%;width:15em}#merge_invoices{display:none;margin:1em auto}#merge{margin:.5em 0 0}#merge_table tr.active td{background-color:#ffc}.renewals{display:block;font-size:.8em;padding:.5em}#transport-types{padding-top:.5px}#i18nMenu .navbar-text .currentlanguage{color:#000;font-weight:700}#i18nMenu a.currentlanguage:link,#i18nMenu a.currentlanguage:visited{font-weight:700}#i18nMenu a .sublanguage-selected{color:#000;font-weight:700}#circ_circulation_issue .onsite_checkout-select,.onsite_checkout-select label{font-size:inherit;font-weight:400}.onsite_checkout{color:#c00}.onsite-checkout-only{background-color:rgba(255,242,206,.5);border:1px solid #fff2ce;border-radius:4px}.branchgriditem{background-color:#fff;border:1px solid #b9d8d9;border-radius:3px;display:table-cell;float:left;margin:3px;padding:.3em}.branchgridrow{display:table-row}.branchselector{display:table}.hq-author{font-weight:700}#cn_browser_table_wrapper>#cn_browser_table{margin:auto;width:90%}#new_rule{background-color:#f4f8f9;border:2px solid #b9d8d9;border-radius:5px;display:none;margin:.3em;padding:.3em}.blocks{margin-bottom:.3em}.remove_rule{font-size:80%;padding-left:.7em}.underline{text-decoration:underline}.overline{text-decoration:overline}.order-control{padding-right:5px}#borrower_message{margin-top:10px}.form-group{margin-bottom:10px}.form-group label{font-weight:700}.modal-textarea{width:98%}#pat_member #patron_list_dialog,#pat_member #searchresults,#patron_search #filters{display:none}#fixedlengthbuilderaction{border:3px solid #e6f0f2;left:80%;padding:5px;position:relative;top:-80px;width:12%}.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background:#e6f0f2 none;box-shadow:none}.navbar-default.navbar-fixed-bottom .navbar-nav>.open>a:focus,.navbar-default.navbar-fixed-bottom .navbar-nav>.open>a:hover{background:transparent none;box-shadow:none}#interlibraryloans #dataPreviewLabel{margin:.3em 0}#interlibraryloans h1{margin:1em 0}#interlibraryloans h2{margin-bottom:20px}#interlibraryloans h3{margin-top:20px}#interlibraryloans .bg-info{overflow:auto;position:relative}#interlibraryloans .format h4{margin-bottom:20px}#interlibraryloans .format h5{margin-top:20px}#interlibraryloans .format input{margin:10px 0}#interlibraryloans .format li{list-style:none}#interlibraryloans #add-new-fields{margin:1em}#interlibraryloans #column-toggle,#interlibraryloans #reset-toggle{font-weight:700;line-height:1.5em;margin:15px 0}#interlibraryloans #freeform-fields .custom-name{margin-right:1em;text-align:right;width:9em}#interlibraryloans #freeform-fields .delete-new-field{margin-left:1em}#interlibraryloans #search-summary{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}#ill-view-panel{margin-top:15px}#ill-view-panel h3{margin-bottom:10px}#ill-view-panel h4{margin-bottom:20px}#ill-view-panel .notesopac{display:inline-block}#ill-view-panel .rows div{height:1em;margin-bottom:1em}#requestattributes{font-family:monospace;line-height:1.3em}#ill-requests{width:100%!important}#helper span,#logged-in-info-full{display:none}.loggedin-menu-label{color:#777;font-size:12px;line-height:1.42857143;padding:4px 12px;white-space:nowrap}.loggedin-menu-label span{color:#000;font-weight:700}.loggedin-menu-label.divider{padding:0}.buttons-list{margin-bottom:30px;padding:0}.buttons-list li{list-style-type:none}.buttons-list li a.circ-button{background-color:#f4f8f9;background-position:5px 3px;background-repeat:no-repeat;border:2px solid #b9d8d9;border-radius:6px;box-sizing:content-box;color:#000;display:block;font-size:110%;font-weight:700;margin:.5em 0;max-width:260px;padding:8px;text-decoration:none}.buttons-list li a.circ-button:hover{border-color:#538200;color:#538200}@media (min-width:200px){.navbar-nav>li{float:left}.navbar-right{float:right!important;margin-right:-15px}.navbar-nav{float:left;margin:0}.navbar-nav .open .dropdown-menu{background-color:#fff;border:1px solid rgba(0,0,0,.15);box-shadow:0 6px 12px rgba(0,0,0,.175);float:left;position:absolute;width:auto}.navbar-nav .open .dropdown-menu.dropdown-menu-left{left:auto;right:0}.navbar-nav .open .dropdown-menu.dropdown-menu-right{right:auto}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{background-color:#0081c2;background-image:linear-gradient(180deg,#08c,#0077b3);background-repeat:repeat-x;color:#fff;text-decoration:none}}@media (min-width:800px){#helper i{display:none}#helper span,#logged-in-info-full{display:inline}#logged-in-info-brief,.loggedin-menu-label{display:none}} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >index 9f5c0cb9e2..29f8d67524 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >@@ -40,5 +40,6 @@ > [% IF ( issuehistoryview ) %]<li class="active">[% ELSE %]<li>[% END %] > <a href="/cgi-bin/koha/catalogue/issuehistory.pl?biblionumber=[% biblio_object_id | url %]" >Checkout history</a></li> > [% IF ( CAN_user_tools_view_system_logs ) %][% IF ( logview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/tools/viewlog.pl?do_it=1&modules=CATALOGUING&action=MODIFY&object=[% biblio_object_id | url %]">Modification log</a> </li>[% END %] >+[% IF ( CAN_user_stockrotation_manage_rota_items && Koha.Preference('StockRotation') ) %][% IF ( stockrotationview ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/catalogue/stockrotation.pl?biblionumber=[% biblio_object_id %]">Rota</a> </li>[% END %] > </ul> > </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >index 81efab0c12..76009c87eb 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/permissions.inc >@@ -118,8 +118,7 @@ > [%# self_check %] > [%- CASE 'self_checkin_module' -%]<span>Log into the self check-in module. Note: this permission prevents the patron from using any other OPAC functionality</span> > [%- CASE 'self_checkout_module' -%]<span>Perform self checkout at the OPAC. It should be used for the patron matching the AutoSelfCheckID</span> >+ [%- CASE 'manage_rota_items' -%]<span>Add and remove items from rotas</span> >+ [%- CASE 'manage_rotas' -%]<span>Create, edit and delete rotas</span> > [%- END -%] >- [%- CASE 'can_add_items_rotas' -%]<span>Add and remove items from rotas</span> >- [%- CASE 'can_edit_rotas' -%]<span>Create, edit and delete rotas</span> >- [%- END -%] > [%- END -%] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc >new file mode 100644 >index 0000000000..f574b19978 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc >@@ -0,0 +1,12 @@ >+[% USE Koha %] >+<div id="toolbar" class="btn-toolbar"> >+ [% IF no_op_set %] >+ <a id="addrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota"><i class="fa fa-plus"></i> New rota</a> >+ [% END %] >+ [% IF op == 'manage_stages' %] >+ <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&rota_id=[% rota_id %]"><i class="fa fa-pencil"></i> Edit rota</a> >+ [% END %] >+ [% IF op == 'manage_items' %] >+ <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&rota_id=[% rota_id %]"><i class="fa fa-pencil"></i> Edit rota</a> >+ [% END %] >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >index d3e03a95ad..bef464c37b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >@@ -1,3 +1,5 @@ >+[% USE Koha %] >+ > <div id="navmenu"> > <div id="navmenulist"> > <ul> >@@ -38,6 +40,9 @@ > [% IF ( CAN_user_tools_batch_upload_patron_images ) %] > <li><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></li> > [% END %] >+ [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %] >+ <li><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></li> >+ [% END %] > </ul> > <h5>Catalog</h5> > <ul> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >new file mode 100644 >index 0000000000..5d6238dff0 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt >@@ -0,0 +1,171 @@ >+[% USE Koha %] >+[% USE Branches %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Catalog › Stock rotation details for [% biblio.title %]</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% INCLUDE 'browser-strings.inc' %] >+[% Asset.js("js/browser.js") %] >+<script type="text/javascript"> >+//<![CDATA[ >+ var browser = KOHA.browser('[% searchid %]', parseInt('[% biblionumber %]', 10)); >+ browser.show(); >+//]]> >+</script> >+</head> >+<body id="catalog_stockrotation" class="catalog"> >+[% USE KohaDates %] >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'cat-search.inc' %] >+ >+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/catalogue/search.pl">Catalog</a> › Stock rotation details for <i>[% biblio.title | html %][% FOREACH subtitle IN biblio.subtitles %][% subtitle.subfield %][% END %]</i></div> >+ >+<div id="doc3" class="yui-t2"> >+ >+ <div id="bd"> >+ <div id="yui-main"> >+ <div class="yui-b"> >+ >+<div id="catalogue_detail_biblio"> >+ >+ [% IF no_op_set %] >+ <h1 class="title">Stock rotation details for [% biblio.title | html %]</h1> >+ [% IF rotas.count > 0 && items.size > 0 %] >+ >+ <table class="items_table dataTable no-footer" role="grid"> >+ <thead> >+ <tr> >+ <th>Barcode</th> >+ <th>Callnumber</th> >+ <th>Rota</th> >+ <th>Rota status</th> >+ <th>In transit</th> >+ <th>Stages & duration in days<br>(current stage highlighted)</th> >+ <th> </th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH item IN items %] >+ <tr> >+ <td>[% item.bib_item.barcode %]</td> >+ <td>[% item.bib_item.itemcallnumber %]</td> >+ <td> >+ [% item.rota.title %] >+ </td> >+ <td> >+ [% IF item.rota %] >+ [% IF !item.rota.active %] >+ <span class="highlighted-row"> >+ [% END %] >+ [% IF item.rota.active %] >+ Active >+ [% ELSE %] >+ Inactive >+ [% END %] >+ [% IF !item.rota.active %] >+ </span> >+ [% END %] >+ [% END %] >+ </td> >+ <td> >+ [% IF item.bib_item.get_transfer %] >+ Yes >+ [% ELSE %] >+ No >+ [% END %] >+ </td> >+ <td> >+ [% FOREACH this_stage IN item.stages %] >+ [% IF this_stage.stage_id == item.stockrotationitem.stage.stage_id %] >+ <span class="stage highlight_stage"> >+ [% ELSE %] >+ <span class="stage"> >+ [% END %] >+ [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %]) >+ </span> >+ » >+ [% END %] >+ [% IF item.stages.size > 0 %] >+ <span class="stage"> >+ [% IF item.rota.cyclical %] >+ START >+ [% ELSE %] >+ END >+ [% END %] >+ </span> >+ [% END %] >+ </td> >+ <td class="actions"> >+ [% IF item.stockrotationitem %] >+ [% in_transit = item.bib_item.get_transfer %] >+ [% IF !in_transit && item.stages.size > 1 %] >+ <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-arrow-right"></i> >+ Move to next stage >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-fire"></i> >+ [% IF item.stockrotationitem.indemand %] >+ Remove "In demand" >+ [% ELSE %] >+ Add "In demand" >+ [% END %] >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&stage_id=[% item.stockrotationitem.stage.stage_id %]&item_id=[% item.bib_item.id %]&biblionumber=[% biblionumber %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-trash"></i> >+ Remove from rota >+ </a> >+ [% ELSE %] >+ <form class="rota_select_form" method="post" enctype="multipart/form-data"> >+ <select class="item_select_rota" name="rota_id"> >+ [% FOREACH rota IN rotas %] >+ <option value="[% rota.rota_id %]">[% rota.title %]</option> >+ [% END %] >+ </select> >+ <button class="btn btn-default btn-xs" type="submit"><i class="fa fa-plus"></i> Add to rota</button> >+ <input type="hidden" name="op" value="add_item_to_rota"></input> >+ <input type="hidden" name="item_id" value="[% item.bib_item.id %]"></input> >+ <input type="hidden" name="biblionumber" value="[% biblionumber %]"></input> >+ </form> >+ [% END %] >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ [% IF !items || items.size == 0 %] >+ <h1>No physical items for this record</h1> >+ [% END %] >+ [% IF !rotas || rotas.count == 0 %] >+ <h1>There are no rotas with stages assigned</h1> >+ [% END %] >+ [% ELSIF op == 'confirm_remove_from_rota' %] >+ <div class="dialog alert"> >+ <h3>Are you sure you want to remove this item from it's rota?</h3> >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&stage_id=[% stage_id %]&item_id=[% item_id %]&biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?biblionumber=[% biblionumber %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% END %] >+ >+</div> >+ >+</div> >+</div> >+<div class="yui-b"> >+[% INCLUDE 'biblio-view-menu.inc' %] >+</div> >+</div> >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt >new file mode 100644 >index 0000000000..9dec3d93b2 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt >@@ -0,0 +1,510 @@ >+[% USE Koha %] >+[% USE Branches %] >+[% USE KohaDates %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Stock rotation</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% Asset.css("css/datatables.css") %] >+[% INCLUDE 'datatables.inc' %] >+[% Asset.js("js/pages/stockrotation.js") %] >+<script type="text/javascript"> >+//<![CDATA[ >+ $(document).ready(function() { >+ $('#sr_manage_items').dataTable($.extend(true, {}, dataTablesDefaults, { >+ "autoWidth": false, >+ "aoColumnDefs": [ >+ { "bSortable": false, "bSearchable": false, 'aTargets': [ 'NoSort' ] }, >+ { "bSortable": true, "bSearchable": false, 'aTargets': [ 'NoSearch' ] } >+ ], >+ "sPaginationType": "four_button" >+ })); >+ }); >+//]]> >+</script> >+</head> >+ >+<body> >+[% INCLUDE 'header.inc' %] >+[% INCLUDE 'patron-search.inc' %] >+ >+<div id="breadcrumbs"> >+ <a href="/cgi-bin/koha/mainpage.pl">Home</a> >+ › <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ >+[% IF no_op_set %] >+ › Stock rotation >+[% ELSE %] >+ › <a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a> >+[% END %] >+ >+[% IF (op == 'create_edit_rota' && rota.rota_id) %] >+ › Edit rota >+[% ELSIF (op == 'create_edit_rota' && !rota.rota_id) %] >+ › Create rota >+[% ELSIF (op == 'manage_stages') %] >+ › Manage stages >+[% ELSIF (op == 'create_edit_stage' && stage.id) %] >+ <a href="?op=manage_stages&rota_id=[% rota_id %]">› Manage stages</a> >+ › Edit stage >+[% ELSIF (op == 'create_edit_stage' && !stage.id) %] >+ <a href="?op=manage_stages&rota_id=[% rota_id %]">› Manage stages</a> >+ › Create stage >+[% ELSIF (op == 'manage_items') %] >+ › Manage items >+[% END %] >+ >+</div> >+ >+<div id="doc3" class="yui-t2"> >+ <div id="bd"> >+ <div id="yui-main"> >+ <div id="stockrotation" class="yui-b"> >+ >+ [% IF no_op_set %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ <h2>Stock rotation</h2> >+ >+ [% IF existing_rotas.size > 0 %] >+ <table class="rotas_table" role="grid"> >+ <thead> >+ <th>Name</th> >+ <th>Cyclical</th> >+ <th>Active</th> >+ <th>Description</th> >+ <th>Number of items</th> >+ <th> </th> >+ </thead> >+ <tbody> >+ [% FOREACH rota IN existing_rotas %] >+ <tr> >+ <td>[% rota.title %]</td> >+ <td>[% rota.cyclical ? 'Yes' : 'No'%]</td> >+ <td>[% rota.active ? 'Yes' : 'No'%]</td> >+ <td>[% rota.description %]</td> >+ <td>[% rota.stockrotationitems.count %]</td> >+ <td class="actions"> >+ <a class="btn btn-default btn-xs" href="?op=create_edit_rota&rota_id=[% rota.rota_id %]"> >+ <i class="fa fa-pencil"></i> >+ Edit >+ </a> >+ <div class="btn-group" role="group"> >+ <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> >+ Manage >+ <i class="fa fa-caret-down"></i> >+ </button> >+ <ul class="dropdown-menu"> >+ <li><a href="?op=manage_stages&rota_id=[% rota.rota_id %]">Stages</a></li> >+ [% IF CAN_user_stockrotation_manage_rota_items && rota.stockrotationstages.count > 0 %] >+ <li><a href="?op=manage_items&rota_id=[% rota.rota_id %]">Items</a></li> >+ [% END %] >+ </ul> >+ </div> >+ <a class="btn btn-default btn-xs" href="?op=toggle_rota&rota_id=[% rota.rota_id %]"> >+ <i class="fa fa-power-off"></i> >+ [% IF !rota.active %] >+ Activate >+ [% ELSE %] >+ Deactivate >+ [% END %] >+ </a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ >+ [% ELSIF (op == 'create_edit_rota') %] >+ >+ [% IF rota.rota_id %] >+ <h2>Edit "[% rota.title %]"</h2> >+ [% ELSE %] >+ <h2>Create new rota</h2> >+ [% END %] >+ >+ [% IF error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <form id="rota_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="title">Name:</label> >+ <input type="text" id="title" name="title" value="[% rota.title %]" required="required" placeholder="Rota name"> >+ <span class="required">Required</span> >+ </li> >+ <li> >+ <label for="cyclical">Cyclical:</label> >+ <select name="cyclical" id="cyclical"> >+ [% IF rota.cyclical %] >+ <option value="1" selected>Yes</option> >+ <option value="0">No</option> >+ [% ELSE %] >+ <option value="1">Yes</option> >+ <option value="0" selected>No</option> >+ [% END %] >+ </select> >+ </li> >+ <li> >+ <label for="description">Description:</label> >+ <textarea id="description" name="description" placeholder="Rota description">[% rota.description %]</textarea> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ <a href="/cgi-bin/koha/tools/stockrotation.pl" class="cancel">Cancel</a> >+ </fieldset> >+ [% IF rota.rota_id %] >+ <input type="hidden" name="id" value="[% rota.rota_id %]"> >+ [% END %] >+ <input type="hidden" name="op" value="process_rota"> >+ </form> >+ >+ [% ELSIF (op == 'manage_stages') %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ [% IF error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <h2>Manage [% rota.title %] stages</h2> >+ <div id="ajax_status" >+ data-saving-msg="Saving changes..." >+ data-success-msg="" >+ data-failed-msg="Error: "> >+ <span id="ajax_saving_msg"></span> >+ <i id="ajax_saving_icon" class="fa fa-spinner fa-spin"></i> >+ <i id="ajax_success_icon" class="fa fa-check"></i> >+ <i id="ajax_failed_icon" class="fa fa-times"></i> >+ <span id="ajax_success_msg"></span> >+ <span id="ajax_failed_msg"></span> >+ </div> >+ >+ <form id="stage_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <legend>Add stage</legend> >+ <ol> >+ <li> >+ <label class="required" for="branch">Library:</label> >+ <select name="branchcode" id="branch"> >+ [% FOREACH branch IN branches %] >+ [% IF branch.branchcode == stage.branchcode_id %] >+ <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option> >+ [% ELSE %] >+ <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ <span class="required">Required</span> >+ </li> >+ <li> >+ <label class="required" for="duration">Duration:</label> >+ <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)"> >+ <span class="required">Required</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Submit"> >+ </fieldset> >+ <input type="hidden" name="stage_id" value="[% stage.id %]"> >+ <input type="hidden" name="rota_id" value="[% rota_id %]"> >+ <input type="hidden" name="op" value="process_stage"> >+ </form> >+ >+ [% IF existing_stages.size > 0 %] >+ <div id="manage_stages"> >+ <div id="manage_stages_help"> >+ Stages can be re-ordered by using the <i class="drag_handle fa fa-lg fa-bars"></i>handle to drag and drop them to their new position >+ </div> >+ <div id="stage_list_headings"> >+ <span class="stagename">Library</span> >+ <span class="stageduration">Duration (days)</span> >+ </div> >+ <ul id="sortable_stages" data-rota-id="[% rota.rota_id %]"> >+ [% FOREACH stage IN existing_stages %] >+ <li id="stage_[% stage.stage_id %]"> >+ <span data-toggle="tooltip" title="Drag and drop to move this stage to another position" data-placement="right" class="stagename"> >+ [% IF existing_stages.size > 1 %] >+ <i class="drag_handle fa fa-lg fa-bars"></i> >+ [% END %] >+ [% Branches.GetName(stage.branchcode_id) %] >+ </span> >+ <span class="stageduration">[% stage.duration %]</span> >+ <span class="stageactions"> >+ <a class="btn btn-default btn-xs" href="?op=create_edit_stage&stage_id=[% stage.stage_id %]"> >+ <i class="fa fa-pencil"></i> Edit >+ </a> >+ <a class="btn btn-default btn-xs" href="?op=confirm_delete_stage&stage_id=[% stage.stage_id %]"> >+ <i class="fa fa-trash"></i> Delete >+ </a> >+ </span> >+ </li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ <p><a href="stockrotation.pl">Return to rotas</a></p> >+ >+ [% ELSIF (op == 'create_edit_stage') %] >+ >+ [% IF stage.id %] >+ <h2>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h2> >+ [% ELSE %] >+ <h2>Create new stage</h2> >+ [% END %] >+ >+ [% IF error == 'invalid_form' %] >+ <div class="dialog alert"> >+ <h3>There was a problem with your form submission</h3> >+ </div> >+ [% END %] >+ >+ <form id="stage_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="branch">Library:</label> >+ <select name="branchcode" id="branch"> >+ [% FOREACH branch IN branches %] >+ [% IF branch.branchcode == stage.branchcode_id %] >+ <option value="[% branch.branchcode %]" selected>[% Branches.GetName(branch.branchcode) %]</option> >+ [% ELSE %] >+ <option value="[% branch.branchcode %]">[% Branches.GetName(branch.branchcode) %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ <span class="required">Required</span> >+ </li> >+ <li> >+ <label class="required" for="duration">Duration:</label> >+ <input type="text" id="duration" name="duration" value="[% stage.duration %]" required="required" placeholder="Duration (days)"> >+ <span class="required">Required</span> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ <a href="/cgi-bin/koha/tools/stockrotation.pl?op=manage_stages&rota_id=[% rota_id %]" class="cancel">Cancel</a> >+ </fieldset> >+ <input type="hidden" name="stage_id" value="[% stage.id %]"> >+ <input type="hidden" name="rota_id" value="[% rota_id %]"> >+ <input type="hidden" name="op" value="process_stage"> >+ </form> >+ [% ELSIF (op == 'confirm_remove_from_rota') %] >+ >+ <div class="dialog alert"> >+ <h3>Are you sure you wish to remove this item from it's rota</h3> >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=remove_item_from_stage&item_id=[% item_id %]&stage_id=[% stage_id %]&rota_id=[% rota_id %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?op=manage_items&rota_id=[% rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% ELSIF (op == 'confirm_delete_stage') %] >+ >+ <div class="dialog alert"> >+ <h3>Are you sure you want to delete this stage?</h3> >+ [% IF stage.stockrotationitems.count > 0 %] >+ <p>This stage contains the following item(s):</p> >+ <ul> >+ [% FOREACH item IN stage.stockrotationitems %] >+ <li>[% item.itemnumber.biblio.title %] (Barcode: [% item.itemnumber.barcode %])</li> >+ [% END %] >+ </ul> >+ [% END %] >+ <p> >+ <a class="btn btn-default btn-xs approve" href="?op=delete_stage&stage_id=[% stage.stage_id %]"><i class="fa fa-fw fa-check"></i>Yes</a> >+ <a class="btn btn-default btn-xs deny" href="?op=manage_stages&rota_id=[% stage.rota.rota_id %]"><i class="fa fa-fw fa-remove"></i>No</a> >+ </p> >+ </div> >+ [% ELSIF (op == 'manage_items') %] >+ >+ [% INCLUDE 'stockrotation-toolbar.inc' %] >+ >+ [% IF error %] >+ <div class="dialog alert"> >+ [% IF error == "item_not_found" %] >+ <h3>The item was not found</h3> >+ [% ELSIF error == "already_on_rota" %] >+ <h3>This item is already on this rota</h3> >+ [% END %] >+ </div> >+ [% END %] >+ >+ <h2>Manage [% rota.title %] items</h2> >+ >+ <div> >+ <form id="add_rota_item_form" method="post" enctype="multipart/form-data" class="validated"> >+ <fieldset class="rows"> >+ <legend>Add item to "[% rota.title %]"</legend> >+ <ol> >+ <li> >+ <label for="barcode">Barcode:</label> >+ <input type="text" id="barcode" name="barcode" placeholder="Item barcode" autofocus> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="rows"> >+ <legend>Use a barcode file</legend> >+ <ol> >+ <li> >+ <label for="barcodefile">Barcode file:</label> >+ <input type="file" id="barcodefile" name="barcodefile"> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ </fieldset> >+ <input type="hidden" name="rota_id" value="[% rota.id %]"> >+ <input type="hidden" name="op" value="add_items_to_rota"> >+ </form> >+ </div> >+ >+ [% IF items.count > 0 %] >+ <h3>Manage items assigned to "[% rota.title %]"</h3> >+ <table id="sr_manage_items" class="items_table" role="grid"> >+ <thead> >+ <th>Barcode</th> >+ <th>Title</th> >+ <th>Author</th> >+ <th>Callnumber</th> >+ <th class="NoSearch">In transit</th> >+ <th class="NoSort">Stages & duration in days<br>(current stage highlighted)</th> >+ <th class="NoSort"> </th> >+ </thead> >+ <tbody> >+ [% FOREACH item IN items %] >+ <tr> >+ <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% item.id %]&biblionumber=[% item.itemnumber.biblio.id %]#item[% item.id %]">[% item.itemnumber.barcode %]</a></td> >+ <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% item.itemnumber.biblio.id %]">[% item.itemnumber.biblio.title %]</a></td> >+ <td>[% item.itemnumber.biblio.author %]</td> >+ <td>[% item.itemnumber.itemcallnumber %]</td> >+ <td>[% item.itemnumber.get_transfer ? 'Yes' : 'No' %]</td> >+ <td> >+ [% FOREACH this_stage IN stages %] >+ [% IF this_stage.stage_id == item.stage.stage_id %] >+ <span class="stage highlight_stage"> >+ [% ELSE %] >+ <span class="stage"> >+ [% END %] >+ [% Branches.GetName(this_stage.branchcode_id) %] ([% this_stage.duration %]) >+ </span> >+ » >+ [% END %] >+ [% IF stages.size > 0 %] >+ <span class="stage">[% rota.cyclical ? 'START' : 'END' %]</span> >+ [% END %] >+ </td> >+ <td class="actions"> >+ [% in_transit = item.itemnumber.get_transfer %] >+ [% IF !in_transit && stages.size > 1 %] >+ <a class="btn btn-default btn-xs" href="?op=move_to_next_stage&rota_id=[% rota.id %]&item_id=[% item.id %]&stage_id=[% item.stage.stage_id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-arrow-right"></i> >+ Move to next stage >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=toggle_in_demand&stage_id=[% item.stage.stage_id %]&item_id=[% item.id %]&rota_id=[% rota.id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-fire"></i> >+ [% item.indemand ? 'Remove "In demand"' : 'Add "In demand"' %] >+ </a> >+ [% IF !in_transit %] >+ <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&stage_id=[% item.stage.stage_id %]&item_id=[% item.id %]&rota_id=[% rota.id %]"> >+ [% ELSE %] >+ <a class="btn btn-default btn-xs" disabled> >+ [% END %] >+ <i class="fa fa-trash"></i> >+ Remove from rota >+ </a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% END %] >+ >+ <p><a href="stockrotation.pl">Return to rotas</a></p> >+ >+ [% ELSIF op == 'add_items_to_rota' %] >+ >+ <div class="dialog message"> >+ <h3>Add items to rota report</h3> >+ </div> >+ >+ <div> >+ [% IF barcode_status.ok.size > 0 %] >+ <h4>Items added to rota:</h4> >+ <ul> >+ [% FOREACH item_ok IN barcode_status.ok %] >+ <li>[% item_ok.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.on_this.size > 0 %] >+ <h4>Items already on this rota:</h4> >+ <ul> >+ [% FOREACH item_on_this IN barcode_status.on_this %] >+ <li>[% item_on_this.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.not_found.size > 0 %] >+ <h4>Barcodes not found:</h4> >+ <ul> >+ [% FOREACH barcode_not_found IN barcode_status.not_found %] >+ <li>[% barcode_not_found %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ [% IF barcode_status.on_other.size > 0 %] >+ <h4>Items found on other rotas:</h4> >+ <ul> >+ [% FOREACH item_on_other IN barcode_status.on_other %] >+ <li>[% item_on_other.biblio.title %]</li> >+ [% END %] >+ </ul> >+ [% END %] >+ </div> >+ [% IF barcode_status.on_other.size > 0 %] >+ <form id="add_rota_item_form" method="post" enctype="multipart/form-data"> >+ <fieldset> >+ <legend>Select items to move to this rota:</legend> >+ [% FOREACH item_on_other IN barcode_status.on_other %] >+ <li><input type="checkbox" name="move_item" value="[% item_on_other.itemnumber %]"> [% item_on_other.biblio.title %] (Currently on "[% item_on_other.stockrotationitem.stage.rota.title %]")</li> >+ [% END %] >+ >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" value="Save"> >+ </fieldset> >+ <input type="hidden" name="rota_id" value="[% rota_id %]"> >+ <input type="hidden" name="op" value="move_items_to_rota"> >+ </form> >+ [% END %] >+ <p><a href="?op=manage_items&rota_id=[% rota_id %]">Return to rota</a></p> >+ >+ [% END %] >+ </div> >+ </div> >+ <div class="yui-b"> >+ [% INCLUDE 'tools-menu.inc' %] >+ </div> >+ </div> >+</div> >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >index a035ee146b..3b3cf011c7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >@@ -1,3 +1,5 @@ >+[% USE Koha %] >+ > [% INCLUDE 'doc-head-open.inc' %] > <title>Koha › Tools</title> > [% INCLUDE 'doc-head-close.inc' %] >@@ -66,6 +68,11 @@ > [% IF ( CAN_user_tools_batch_upload_patron_images ) %] > <dt><a href="/cgi-bin/koha/tools/picture-upload.pl">Upload patron images</a></dt> > <dd>Upload patron images in a batch or one at a time</dd> >+ [% END %] >+ >+ [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %] >+ <dt><a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a></dt> >+ <dd>Manage Stock rotation rotas, rota stages and rota items</dd> > [% END %] > </dl> > </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js b/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js >new file mode 100644 >index 0000000000..276394518b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/pages/stockrotation.js >@@ -0,0 +1,65 @@ >+function init() { >+ $('#ajax_status').hide(); >+ $('#ajax_saving_msg').hide(); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_success_icon').hide(); >+ $('#ajax_failed_icon').hide(); >+ $('#ajax_failed_msg').hide(); >+} >+ >+$(document).ready(function() { >+ var apiEndpoint = '/api/v1/rotas/'; >+ init(); >+ $('#sortable_stages').sortable({ >+ handle: '.drag_handle', >+ placeholder: 'drag_placeholder', >+ update: function(event, ui) { >+ init(); >+ $('#sortable_stages').sortable('disable'); >+ var rotaId = document.getElementById('sortable_stages').dataset.rotaId; >+ $('#ajax_saving_msg').text( >+ document.getElementById('ajax_status').dataset.savingMsg >+ ); >+ $('#ajax_saving_icon').show(); >+ $('#ajax_saving_msg').show(); >+ $('#ajax_status').fadeIn(); >+ var stageId = ui.item[0].id.replace(/^stage_/, ''); >+ var newIndex = ui.item.index(); >+ var newPosition = newIndex + 1; >+ $.ajax({ >+ method: 'PUT', >+ url: apiEndpoint + rotaId + '/stages/' + stageId + '/position', >+ processData: false, >+ contentType: 'application/json', >+ data: newPosition >+ }) >+ .done(function(data) { >+ $('#ajax_success_msg').text( >+ document.getElementById('ajax_status').dataset.successMsg >+ ); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_success_icon').show(); >+ $('#ajax_success_msg').show(); >+ setTimeout( >+ function() { >+ $('#ajax_status').fadeOut(); >+ }, >+ 700 >+ ); >+ }) >+ .fail(function(jqXHR, status, error) { >+ $('#ajax_failed_msg').text( >+ document.getElementById('ajax_status').dataset.failedMsg + >+ error >+ ); >+ $('#ajax_saving_icon').hide(); >+ $('#ajax_failed_icon').show(); >+ $('#ajax_failed_msg').show(); >+ $('#sortable_stages').sortable('cancel'); >+ }) >+ .always(function() { >+ $('#sortable_stages').sortable('enable'); >+ }) >+ } >+ }); >+}); >diff --git a/misc/cronjobs/stockrotation.pl b/misc/cronjobs/stockrotation.pl >new file mode 100755 >index 0000000000..a6cd0c12b1 >--- /dev/null >+++ b/misc/cronjobs/stockrotation.pl >@@ -0,0 +1,528 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS Europe >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 NAME >+ >+stockrotation.pl >+ >+=head1 SYNOPSIS >+ >+ --[a]dmin-email An address to which email reports should also be sent >+ --[b]ranchcode Select branch to report on for 'email' reports (default: all) >+ --e[x]ecute Actually perform stockrotation housekeeping >+ --[r]eport Select either 'full' or 'email' >+ --[S]end-all Send email reports even if the report body is empty >+ --[s]end-email Send reports by email >+ --[h]elp Display this help message >+ >+Cron script implementing scheduled stockrotation functionality. >+ >+By default this script merely reports on the current status of the >+stockrotation subsystem. In order to actually place items in transit, the >+script must be run with the `execute` argument. >+ >+`report` allows you to select the type of report that will be emitted. It's >+set to 'full' by default. If the `email` report is selected, you can use the >+`branchcode` parameter to specify which branch's report you would like to see. >+The default is 'all'. >+ >+`admin-email` is an additional email address to which we will send all email >+reports in addition to sending them to branch email addresses. >+ >+`send-email` will cause the script to send reports by email, and `send-all` >+will cause even reports with an empty body to be sent. >+ >+=head1 DESCRIPTION >+ >+This script is used to move items from one stockrotationstage to the next, >+if they are elible for processing. >+ >+it should be run from cron like: >+ >+ stockrotation.pl --report email --send-email --execute >+ >+Prior to that you can run the script from the command line without the >+--execute and --send-email parameters to see what reports the script would >+generate in 'production' mode. This is immensely useful for testing, or for >+getting to understand how the stockrotation module works: you can set up >+different scenarios, and then "query" the system on what it would do. >+ >+Normally you would want to run this script once per day, probably around >+midnight-ish to move any stockrotationitems along their rotas and to generate >+the email reports for branch libraries. >+ >+Each library will receive a report with "items of interest" for them for >+today's rota checks. Each item there will be an item that should, according >+to Koha, be located on the shelves of that branch, and which should be picked >+up and checked in. The item will either: >+- have been placed in transit to their new stage library; >+- have been placed in transit to be returned to their current stage library; >+- have just been added to a rota and will already be at the correct library; >+ >+In the last case the item will be checked in and no message will pop up. In >+the other cases a message will pop up requesting the item be posted to their >+new branch. >+ >+=head2 What does the --execute flag do? >+ >+To understand this, you will need to know a little bit about the design of >+this script and the stockrotation modules. >+ >+This script operates in 3 phases: first it walks the graph of rotas, stages >+and items. For each active rota, it investigates the items in each stage and >+determines whether action is required. It does not perform any actions, it >+just "sieves" all items on active rotas into "actionable" and "non-actionable" >+baskets. We can use these baskets to perform actions against the items, or to >+generate reports. >+ >+During the second phase this script then loops through the actionable baskets, >+and performs the relevant action (initiate, repatriate, advance) on each item. >+ >+Finally, during the third phase we revisit the original baskets and we compile >+reports (for instance per branch email reports). >+ >+When the script is run without the "--execute" flag, we perform phase 1, skip >+phase 2 and move straight onto phase 3. >+ >+With the "--execute" flag we also perform the database operations. >+ >+So with or without the flag, the report will look the same (except for the "No >+database updates have been performed."). >+ >+=cut >+ >+use Modern::Perl; >+use Getopt::Long qw/HelpMessage :config gnu_getopt/; >+use C4::Context; >+use C4::Letters; >+use Koha::StockRotationRotas; >+ >+my $admin_email = ''; >+my $branch = 0; >+my $execute = 0; >+my $report = 'full'; >+my $send_all = 0; >+my $send_email = 0; >+ >+my $ok = GetOptions( >+ 'admin-email|a=s' => \$admin_email, >+ 'branchcode|b=s' => sub { >+ my ( $opt_name, $opt_value ) = @_; >+ my $branches = Koha::Libraries->search( {}, >+ { order_by => { -asc => 'branchname' } } ); >+ my $brnch = $branches->find($opt_value); >+ if ($brnch) { >+ $branch = $brnch; >+ return $brnch; >+ } >+ else { >+ printf("Option $opt_name should be one of (name -> code):\n"); >+ while ( my $candidate = $branches->next ) { >+ printf( " %-40s -> %s\n", >+ $candidate->branchname, $candidate->branchcode ); >+ } >+ exit 1; >+ } >+ }, >+ 'execute|x' => \$execute, >+ 'report|r=s' => sub { >+ my ( $opt_name, $opt_value ) = @_; >+ if ( $opt_value eq 'full' || $opt_value eq 'email' ) { >+ $report = $opt_value; >+ } >+ else { >+ printf("Option $opt_name should be either 'email' or 'full'.\n"); >+ exit 1; >+ } >+ }, >+ 'send-all|S' => \$send_all, >+ 'send-email|s' => \$send_email, >+ 'help|h|?' => sub { HelpMessage } >+); >+exit 1 unless ($ok); >+ >+$send_email++ if ($send_all); # if we send all, then we must want emails. >+ >+=head2 Helpers >+ >+=head3 execute >+ >+ undef = execute($report); >+ >+Perform the database updates, within a transaction, that are reported as >+needing to be performed by $REPORT. >+ >+$REPORT should be the return value of an invocation of `investigate`. >+ >+This procedure WILL mess with your database. >+ >+=cut >+ >+sub execute { >+ my ($data) = @_; >+ >+ # Begin transaction >+ my $schema = Koha::Database->new->schema; >+ $schema->storage->txn_begin; >+ >+ # Carry out db updates >+ foreach my $item ( @{ $data->{items} } ) { >+ my $reason = $item->{reason}; >+ if ( $reason eq 'repatriation' ) { >+ $item->{object}->repatriate; >+ } >+ elsif ( grep { $reason eq $_ } qw/in-demand advancement initiation/ ) { >+ $item->{object}->advance; >+ } >+ } >+ >+ # End transaction >+ $schema->storage->txn_commit; >+} >+ >+=head3 report_full >+ >+ my $full_report = report_full($report); >+ >+Return an arrayref containing a string containing a detailed report about the >+current state of the stockrotation subsystem. >+ >+$REPORT should be the return value of `investigate`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub report_full { >+ my ($data) = @_; >+ >+ my $header = ""; >+ my $body = ""; >+ >+ # Summary >+ $header .= sprintf " >+STOCKROTATION REPORT >+--------------------\n"; >+ $body .= sprintf " >+ Total number of rotas: %5u >+ Inactive rotas: %5u >+ Active rotas: %5u >+ Total number of items: %5u >+ Inactive items: %5u >+ Stationary items: %5u >+ Actionable items: %5u >+ Total items to be initiated: %5u >+ Total items to be repatriated: %5u >+ Total items to be advanced: %5u >+ Total items in demand: %5u\n\n", >+ $data->{sum_rotas}, $data->{rotas_inactive}, $data->{rotas_active}, >+ $data->{sum_items}, $data->{items_inactive}, $data->{stationary}, >+ $data->{actionable}, $data->{initiable}, $data->{repatriable}, >+ $data->{advanceable}, $data->{indemand}; >+ >+ if ( @{ $data->{rotas} } ) { # Per Rota details >+ $body .= sprintf "ROTAS DETAIL\n------------\n\n"; >+ foreach my $rota ( @{ $data->{rotas} } ) { >+ $body .= sprintf "Details for %s [%s]:\n", >+ $rota->{name}, $rota->{id}; >+ $body .= sprintf "\n Items:"; # Rota item details >+ if ( @{ $rota->{items} } ) { >+ $body .= >+ join( "", map { _print_item($_) } @{ $rota->{items} } ); >+ } >+ else { >+ $body .= >+ sprintf "\n No items to be processed for this rota.\n"; >+ } >+ $body .= sprintf "\n Log:"; # Rota log details >+ if ( @{ $rota->{log} } ) { >+ $body .= join( "", map { _print_item($_) } @{ $rota->{log} } ); >+ } >+ else { >+ $body .= sprintf "\n No items in log for this rota.\n\n"; >+ } >+ } >+ } >+ return [ >+ $header, >+ { >+ letter => { >+ title => 'Stockrotation Report', >+ content => $body # The body of the report >+ }, >+ status => 1, # We have a meaningful report >+ no_branch_email => 1, # We don't expect branch email in report >+ } >+ ]; >+} >+ >+=head3 report_email >+ >+ my $email_report = report_email($report); >+ >+Returns an arrayref containing a header string, with basic report information, >+and any number of 'per_branch' strings, containing a detailed report about the >+current state of the stockrotation subsystem, from the perspective of those >+individual branches. >+ >+$REPORT should be the return value of `investigate`, and $BRANCH should be >+either 0 (to indicate 'all'), or a specific Koha::Library object. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub report_email { >+ my ( $data, $branch ) = @_; >+ >+ my $out = []; >+ my $header = ""; >+ >+ # Summary >+ my $branched = $data->{branched}; >+ my $flag = 0; >+ >+ $header .= sprintf " >+BRANCH-BASED STOCKROTATION REPORT >+---------------------------------\n"; >+ push @{$out}, $header; >+ >+ if ($branch) { # Branch limited report >+ push @{$out}, _report_per_branch( $branched->{ $branch->branchcode } ); >+ } >+ elsif ( $data->{actionable} ) { # Full email report >+ while ( my ( $branchcode_id, $details ) = each %{$branched} ) { >+ push @{$out}, _report_per_branch($details) >+ if ( @{ $details->{items} } ); >+ } >+ } >+ else { >+ push @{$out}, { >+ body => sprintf " >+No actionable items at any libraries.\n\n", # The body of the report >+ no_branch_email => 1, # We don't expect branch email in report >+ }; >+ } >+ return $out; >+} >+ >+=head3 _report_per_branch >+ >+ my $branch_string = _report_per_branch($branch_details, $branchcode, $branchname); >+ >+return a string containing details about the stockrotation items and their >+status for the branch identified by $BRANCHCODE. >+ >+This helper procedure is only used from within `report_email`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub _report_per_branch { >+ my ($branch) = @_; >+ >+ my $status = 0; >+ if ( $branch && @{ $branch->{items} } ) { >+ $status = 1; >+ } >+ >+ if ( >+ my $letter = C4::Letters::GetPreparedLetter( >+ module => 'circulation', >+ letter_code => "SR_SLIP", >+ message_transport_type => 'email', >+ substitute => $branch >+ ) >+ ) >+ { >+ return { >+ letter => $letter, >+ email_address => $branch->{email}, >+ $status >+ }; >+ } >+ return; >+} >+ >+=head3 _print_item >+ >+ my $string = _print_item($item_section); >+ >+Return a string containing an overview about $ITEM_SECTION. >+ >+This helper procedure is only used from within `report_full`. >+ >+No data in the database is manipulated by this procedure. >+ >+=cut >+ >+sub _print_item { >+ my ($item) = @_; >+ return sprintf " >+ Title: %s >+ Author: %s >+ Callnumber: %s >+ Location: %s >+ Barcode: %s >+ On loan?: %s >+ Status: %s >+ Current Library: %s [%s]\n\n", >+ $item->{title} || "N/A", $item->{author} || "N/A", >+ $item->{callnumber} || "N/A", $item->{location} || "N/A", >+ $item->{barcode} || "N/A", $item->{onloan} ? 'Yes' : 'No', >+ $item->{reason} || "N/A", $item->{branch}->branchname, >+ $item->{branch}->branchcode; >+} >+ >+=head3 emit >+ >+ undef = emit($params); >+ >+$PARAMS should be a hashref of the following format: >+ admin_email: the address to which a copy of all reports should be sent. >+ execute: the flag indicating whether we performed db updates >+ send_all: the flag indicating whether we should send even empty reports >+ send_email: the flag indicating whether we want to emit to stdout or email >+ report: the data structure returned from one of the report procedures >+ >+No data in the database is manipulated by this procedure. >+ >+The return value is unspecified: we simply emit a message as a side-effect or >+die. >+ >+=cut >+ >+sub emit { >+ my ($params) = @_; >+ >+# REPORT is an arrayref of at least 2 elements: >+# - The header for the report, which will be repeated for each part >+# - a "part" for each report we want to emit >+# PARTS are hashrefs: >+# - part->{status}: a boolean indicating whether the reported part is empty or not >+# - part->{email_address}: the email address to send the report to >+# - part->{no_branch_email}: a boolean indicating that we are missing a branch email >+# - part->{letter}: a GetPreparedLetter hash as returned by the C4::Letters module >+ my $report = $params->{report}; >+ my $header = shift @{$report}; >+ my $parts = $report; >+ >+ my @emails; >+ foreach my $part ( @{$parts} ) { >+ >+ if ( $part->{status} || $params->{send_all} ) { >+ >+ # We have a report to send, or we want to send even empty >+ # reports. >+ >+ # Send to branch >+ my $addressee; >+ if ( $part->{email_address} ) { >+ $addressee = $part->{email_address}; >+ } >+ elsif ( !$part->{no_branch_email} ) { >+ >+#push @emails, "***We tried to send a branch report, but we have no email address for this branch.***\n\n"; >+ $addressee = C4::Context->preference('KohaAdminEmailAddress') >+ if ( C4::Context->preference('KohaAdminEmailAddress') ); >+ } >+ >+ if ( $params->{send_email} ) { # Only email if emails requested >+ if ( defined($addressee) ) { >+ C4::Letters::EnqueueLetter( >+ { >+ letter => $part->{letter}, >+ to_address => $addressee, >+ message_transport_type => 'email', >+ } >+ ) >+ or warn >+ "can't enqueue letter $part->{letter} for $addressee"; >+ } >+ >+ # Copy to admin? >+ if ( $params->{admin_email} ) { >+ C4::Letters::EnqueueLetter( >+ { >+ letter => $part->{letter}, >+ to_address => $params->{admin_email}, >+ message_transport_type => 'email', >+ } >+ ) >+ or warn >+"can't enqueue letter $part->{letter} for $params->{admin_email}"; >+ } >+ } >+ else { >+ my $email = >+ "-------- Email message --------" . "\n\n" . "To: " >+ . defined($addressee) ? $addressee >+ : defined( $params->{admin_email} ) ? $params->{admin_email} >+ : '' . "\n" >+ . "Subject: " >+ . $part->{letter}->{title} . "\n\n" >+ . $part->{letter}->{content}; >+ push @emails, $email; >+ } >+ } >+ } >+ >+ # Emit to stdout instead of email? >+ if ( !$params->{send_email} ) { >+ >+ # The final message is the header + body of this part. >+ my $msg = $header; >+ $msg .= "No database updates have been performed.\n\n" >+ unless ( $params->{execute} ); >+ >+ # Append email reports to message >+ $msg .= join( "\n\n", @emails ); >+ printf $msg; >+ } >+} >+ >+#### Main Code >+ >+# Compile Stockrotation Report data >+my $rotas = Koha::StockRotationRotas->search(undef,{ order_by => { '-asc' => 'title' }}); >+my $data = $rotas->investigate; >+ >+# Perform db updates if requested >+execute($data) if ($execute); >+ >+# Emit Reports >+my $out_report = {}; >+$out_report = report_email( $data, $branch ) if $report eq 'email'; >+$out_report = report_full( $data, $branch ) if $report eq 'full'; >+emit( >+ { >+ admin_email => $admin_email, >+ execute => $execute, >+ report => $out_report, >+ send_all => $send_all, >+ send_email => $send_email, >+ } >+); >+ >+=head1 AUTHOR >+ >+Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> >+ >+=cut >diff --git a/t/db_dependent/Items.t b/t/db_dependent/Items.t >index 92151d662d..a3287351f4 100755 >--- a/t/db_dependent/Items.t >+++ b/t/db_dependent/Items.t >@@ -830,6 +830,67 @@ subtest 'Test logging for ModItem' => sub { > $schema->resultset('ActionLog')->search()->delete(); > ModItem({ location => $location }, $bibnum, $itemnumber); > is( $schema->resultset('ActionLog')->count(), 1, 'Undefined value defaults to true, triggers logging' ); >+}; >+ >+subtest 'Check stockrotationitem relationship' => sub { >+ plan tests => 1; >+ >+ $schema->storage->txn_begin(); >+ >+ my $builder = t::lib::TestBuilder->new; >+ my $item = $builder->build({ source => 'Item' }); >+ >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { itemnumber_id => $item->{itemnumber} } >+ }); >+ >+ my $sritem = Koha::Items->find($item->{itemnumber})->stockrotationitem; >+ isa_ok( $sritem, 'Koha::StockRotationItem', "Relationship works and correctly creates Koha::Object." ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Check add_to_rota method' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin(); >+ >+ my $builder = t::lib::TestBuilder->new; >+ my $item = $builder->build({ source => 'Item' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $sritem = Koha::Items->find($item->{itemnumber}); >+ $sritem->add_to_rota($rota->{rota_id}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srrota->stockrotationstages->next->stage_id, >+ "Adding to a rota a new sritem item being assigned to its first stage." >+ ); >+ >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $newrota->{rota_id} }, >+ }); >+ >+ $sritem->add_to_rota($newrota->{rota_id}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srnewrota->stockrotationstages->next->stage_id, >+ "Moving an item results in that sritem being assigned to the new first stage." >+ ); > > $schema->storage->txn_rollback; > }; >diff --git a/t/db_dependent/Koha/Libraries.t b/t/db_dependent/Koha/Libraries.t >index d24ba3d6fe..ef6a10b6be 100644 >--- a/t/db_dependent/Koha/Libraries.t >+++ b/t/db_dependent/Koha/Libraries.t >@@ -19,7 +19,7 @@ > > use Modern::Perl; > >-use Test::More tests => 4; >+use Test::More tests => 6; > > use Koha::Library; > use Koha::Libraries; >@@ -53,6 +53,29 @@ is( $retrieved_library_1->branchname, $new_library_1->branchname, 'Find a librar > $retrieved_library_1->delete; > is( Koha::Libraries->search->count, $nb_of_libraries + 1, 'Delete should have deleted the library' ); > >+# Stockrotation relationship testing >+ >+my $new_library_sr = $builder->build({ source => 'Branch' }); >+ >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+$builder->build({ >+ source => 'Stockrotationstage', >+ value => { branchcode_id => $new_library_sr->{branchcode} }, >+}); >+ >+my $srstages = Koha::Libraries->find($new_library_sr->{branchcode}) >+ ->stockrotationstages; >+is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this branch'); >+ >+isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." ); >+ > $schema->storage->txn_rollback; > > subtest '->get_effective_marcorgcode' => sub { >diff --git a/t/db_dependent/StockRotationItems.t b/t/db_dependent/StockRotationItems.t >new file mode 100644 >index 0000000000..2d9bf89a39 >--- /dev/null >+++ b/t/db_dependent/StockRotationItems.t >@@ -0,0 +1,393 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use DateTime; >+use DateTime::Duration; >+use Koha::Database; >+use Koha::Item::Transfer; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 8; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationItems'); >+use_ok('Koha::StockRotationItem'); >+ >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'Basic object tests' => sub { >+ >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $itm = $builder->build({ source => 'Item' }); >+ my $stage = $builder->build({ source => 'Stockrotationstage' }); >+ >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ itemnumber_id => $itm->{itemnumber}, >+ stage_id => $stage->{stage_id}, >+ }, >+ }); >+ >+ my $sritem = Koha::StockRotationItems->find($item->{itemnumber_id}); >+ isa_ok( >+ $sritem, >+ 'Koha::StockRotationItem', >+ "Correctly create and load a stock rotation item." >+ ); >+ >+ # Relationship to rota >+ isa_ok( $sritem->itemnumber, 'Koha::Item', "Fetched related item." ); >+ is( $sritem->itemnumber->itemnumber, $itm->{itemnumber}, "Related rota OK." ); >+ >+ # Relationship to stage >+ isa_ok( $sritem->stage, 'Koha::StockRotationStage', "Fetched related stage." ); >+ is( $sritem->stage->stage_id, $stage->{stage_id}, "Related stage OK." ); >+ >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Tests for needs_repatriating' => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ # Setup a pristine stockrotation context. >+ my $sritem = $builder->build({ source => 'Stockrotationitem' }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->stage->position(1); >+ >+ my $dbrota = $dbitem->stage->rota; >+ my $newstage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $dbrota->rota_id, >+ position => 2, >+ } >+ }); >+ >+ # - homebranch == holdingbranch [0] >+ is( >+ $dbitem->needs_repatriating, 0, >+ "Homebranch == Holdingbranch." >+ ); >+ >+ my $branch = $builder->build({ source => 'Branch' }); >+ $dbitem->itemnumber->holdingbranch($branch->{branchcode}); >+ >+ # - homebranch != holdingbranch [1] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "Homebranch != holdingbranch." >+ ); >+ >+ # Set to incorrect homebranch. >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->homebranch($branch->{branchcode}); >+ # - homebranch != stockrotationstage.branch & not in transit [1] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "Homebranch != StockRotationStage.Branchcode_id & not in transit." >+ ); >+ >+ # Set to in transit (by implication). >+ $dbitem->stage($newstage->{stage_id}); >+ # - homebranch != stockrotaitonstage.branch & in transit [0] >+ is( >+ $dbitem->needs_repatriating, 1, >+ "homebranch != stockrotaitonstage.branch & in transit." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for repatriate." => sub { >+ plan tests => 3; >+ $schema->storage->txn_begin; >+ my $sritem = $builder->build({ source => 'Stockrotationitem' }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->stage->position(1); >+ $dbitem->stage->duration(50); >+ my $branch = $builder->build({ source => 'Branch' }); >+ $dbitem->itemnumber->holdingbranch($branch->{branchcode}); >+ >+ # Test a straight up repatriate >+ ok($dbitem->repatriate, "Repatriation done."); >+ my $intransfer = $dbitem->itemnumber->get_transfer; >+ is($intransfer->frombranch, $branch->{branchcode}, "Origin correct."); >+ is($intransfer->tobranch, $dbitem->stage->branchcode_id, "Target Correct."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for needs_advancing." => sub { >+ plan tests => 6; >+ $schema->storage->txn_begin; >+ >+ # Test behaviour of item freshly added to rota. >+ my $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 1, }, >+ }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->needs_advancing, 1, "An item that is fresh will always need advancing."); >+ >+ # Setup a pristine stockrotation context. >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ $dbitem->stage->position(1); >+ $dbitem->stage->duration(50); >+ >+ my $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->stage->branchcode_id, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'datearrived' => undef, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ >+ # Test item will not be advanced if in transit. >+ is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer."); >+ # Test item will not be advanced if in transit even if fresh. >+ $dbitem->fresh(1)->store; >+ is($dbitem->needs_advancing, 0, "Not ready to advance: in transfer (fresh)."); >+ $dbitem->fresh(0)->store; >+ >+ # Test item will not be advanced if it has not spent enough time. >+ $dbtransfer->datearrived(DateTime->now)->store; >+ is($dbitem->needs_advancing, 0, "Not ready to advance: Not spent enough time."); >+ # Test item will be advanced if it has not spent enough time, but is fresh. >+ $dbitem->fresh(1)->store; >+ is($dbitem->needs_advancing, 1, "Advance: Not spent enough time, but fresh."); >+ $dbitem->fresh(0)->store; >+ >+ # Test item will be advanced if it has spent enough time. >+ $dbtransfer->datesent( # Item was sent 100 days ago... >+ DateTime->now - DateTime::Duration->new( days => 100 ) >+ )->store; >+ $dbtransfer->datearrived( # And arrived 75 days ago. >+ DateTime->now - DateTime::Duration->new( days => 75 ) >+ )->store; >+ is($dbitem->needs_advancing, 1, "Ready to be advanced."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for advance." => sub { >+ plan tests => 15; >+ $schema->storage->txn_begin; >+ >+ my $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 1 } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id); >+ my $dbstage = $dbitem->stage; >+ $dbstage->position(1)->duration(50)->store; # Configure stage. >+ # Configure item >+ $dbitem->itemnumber->holdingbranch($dbstage->branchcode_id)->store; >+ $dbitem->itemnumber->homebranch($dbstage->branchcode_id)->store; >+ # Sanity check >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage sanity check."); >+ >+ # Test if an item is fresh, always move to first stage. >+ is($dbitem->fresh, 1, "Fresh is correct."); >+ $dbitem->advance; >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Stage is first stage after fresh advance."); >+ is($dbitem->fresh, 0, "Fresh reset after advance."); >+ >+ # Test cases of single stage >+ $dbstage->rota->cyclical(1)->store; # Set Rota to cyclical. >+ ok($dbitem->advance, "Single stage cyclical advance done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Single stage cyclical stage OK."); >+ >+ # Test with indemand advance >+ $dbitem->indemand(1)->store; >+ ok($dbitem->advance, "Indemand item advance done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->indemand, 0, "Indemand OK."); >+ is($dbitem->stage->stage_id, $dbstage->stage_id, "Indemand item advance stage OK."); >+ >+ # Multi stages >+ my $srstage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { duration => 50 } >+ }); >+ my $dbstage2 = Koha::StockRotationStages->find($srstage->{stage_id}); >+ $dbstage2->move_to_group($dbitem->stage->rota_id); >+ $dbstage2->move_last; >+ >+ # Test a straight up advance >+ ok($dbitem->advance, "Advancement done."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ ## Test results >+ is($dbitem->stage->stage_id, $dbstage2->stage_id, "Stage updated."); >+ my $intransfer = $dbitem->itemnumber->get_transfer; >+ is($intransfer->frombranch, $dbstage->branchcode_id, "Origin correct."); >+ is($intransfer->tobranch, $dbstage2->branchcode_id, "Target Correct."); >+ >+ $dbstage->rota->cyclical(0)->store; # Set Rota to non-cyclical. >+ >+ # Arrive at new branch >+ $intransfer->datearrived(DateTime->now)->store; >+ $dbitem->itemnumber->holdingbranch($srstage->{branchcode_id})->store; >+ $dbitem->itemnumber->homebranch($srstage->{branchcode_id})->store; >+ >+ # Advance again, Remove from rota. >+ ok($dbitem->advance, "Non-cyclical advance."); >+ ## Refetch dbitem >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem, undef, "StockRotationItem has been removed."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest "Tests for investigate (singular)." => sub { >+ plan tests => 7; >+ $schema->storage->txn_begin; >+ >+ # Test brand new item's investigation ['initiation'] >+ my $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } }); >+ my $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ is($dbitem->investigate->{reason}, 'initiation', "fresh item initiates."); >+ >+ # Test brand new item at stagebranch ['initiation'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 1 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'initiation', "fresh item at stagebranch initiates."); >+ >+ # Test item not at stagebranch with branchtransfer history ['repatriation'] >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ my $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->itemnumber->homebranch, >+ 'datesent' => DateTime->now, >+ 'datearrived' => DateTime->now, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ is($dbitem->investigate->{reason}, 'repatriation', "older item repatriates."); >+ >+ # Test item at stagebranch with branchtransfer history ['not-ready'] >+ $sritem = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { 'fresh' => 0,} >+ }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now, >+ 'datearrived' => DateTime->now, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'not-ready', "older item at stagebranch not-ready."); >+ >+ # Test item due for advancement ['advancement'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(0)->store; >+ $dbitem->stage->duration(50)->store; >+ my $sent_duration = DateTime::Duration->new( days => 55); >+ my $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'advancement', >+ "Item ready for advancement."); >+ >+ # Test item due for advancement but in-demand ['in-demand'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(1)->store; >+ $dbitem->stage->duration(50)->store; >+ $sent_duration = DateTime::Duration->new( days => 55); >+ $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ $dbitem->itemnumber->homebranch($dbitem->stage->branchcode_id)->store; >+ $dbitem->itemnumber->holdingbranch($dbitem->stage->branchcode_id)->store; >+ is($dbitem->investigate->{reason}, 'in-demand', >+ "Item advances, but in-demand."); >+ >+ # Test item ready for advancement, but at wrong library ['repatriation'] >+ $sritem = $builder->build({ source => 'Stockrotationitem', value => { fresh => 0 } }); >+ $dbitem = Koha::StockRotationItems->find($sritem->{itemnumber_id}); >+ $dbitem->indemand(0)->store; >+ $dbitem->stage->duration(50)->store; >+ $sent_duration = DateTime::Duration->new( days => 55); >+ $arrived_duration = DateTime::Duration->new( days => 52); >+ $dbtransfer = Koha::Item::Transfer->new({ >+ 'itemnumber' => $dbitem->itemnumber_id, >+ 'frombranch' => $dbitem->itemnumber->homebranch, >+ 'tobranch' => $dbitem->stage->branchcode_id, >+ 'datesent' => DateTime->now - $sent_duration, >+ 'datearrived' => DateTime->now - $arrived_duration, >+ 'comments' => "StockrotationAdvance", >+ })->store; >+ is($dbitem->investigate->{reason}, 'repatriation', >+ "Item advances, but not at stage branch."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/StockRotationRotas.t b/t/db_dependent/StockRotationRotas.t >new file mode 100644 >index 0000000000..f1b2aa6d78 >--- /dev/null >+++ b/t/db_dependent/StockRotationRotas.t >@@ -0,0 +1,175 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 5; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationRotas'); >+use_ok('Koha::StockRotationRota'); >+ >+subtest 'Basic object tests' => sub { >+ >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ isa_ok( >+ $srrota, >+ 'Koha::StockRotationRota', >+ "Correctly create and load a stock rotation rota." >+ ); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $srstages = $srrota->stockrotationstages; >+ is( $srstages->count, 3, 'Correctly fetched stockrotationstages associated with this rota'); >+ >+ isa_ok( $srstages->next, 'Koha::StockRotationStage', "Relationship correctly creates Koha::Objects." ); >+ >+ #### Test add_item >+ >+ my $item = $builder->build({ source => 'Item' }); >+ >+ $srrota->add_item($item->{itemnumber}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srrota->first_stage->stage_id, >+ "Adding an item results in a new sritem item being assigned to the first stage." >+ ); >+ >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $srnewrota = Koha::StockRotationRotas->find($newrota->{rota_id}); >+ >+ $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $newrota->{rota_id} }, >+ }); >+ >+ $srnewrota->add_item($item->{itemnumber}); >+ >+ is( >+ Koha::StockRotationItems->find($item->{itemnumber})->stage_id, >+ $srnewrota->stockrotationstages->next->stage_id, >+ "Moving an item results in that sritem being assigned to the new first stage." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest '->first_stage test' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ my $srstage2 = Koha::StockRotationStages->find($stage2->{stage_id}); >+ my $firststage = $srstage2->first_sibling || $srstage2; >+ >+ is( $srrota->first_stage->stage_id, $firststage->stage_id, "First stage works" ); >+ >+ $srstage2->move_first; >+ >+ is( Koha::StockRotationRotas->find($rota->{rota_id})->first_stage->stage_id, $stage2->{stage_id}, "Stage re-organized" ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest '->items test' => sub { >+ plan tests => 1; >+ >+ $schema->storage->txn_begin; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id} }, >+ }); >+ >+ map { $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $_ }, >+ }) } ( >+ $stage1->{stage_id}, $stage1->{stage_id}, >+ $stage2->{stage_id}, $stage2->{stage_id}, >+ $stage3->{stage_id}, $stage3->{stage_id}, >+ ); >+ >+ my $srrota = Koha::StockRotationRotas->find($rota->{rota_id}); >+ >+ is( >+ $srrota->stockrotationitems->count, >+ 6, "Correct number of items" >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/StockRotationStages.t b/t/db_dependent/StockRotationStages.t >new file mode 100644 >index 0000000000..e6ea69315e >--- /dev/null >+++ b/t/db_dependent/StockRotationStages.t >@@ -0,0 +1,377 @@ >+#!/usr/bin/perl >+ >+# Copyright PTFS Europe 2016 >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Koha::Database; >+use t::lib::TestBuilder; >+ >+use Test::More tests => 6; >+ >+my $schema = Koha::Database->new->schema; >+ >+use_ok('Koha::StockRotationStages'); >+use_ok('Koha::StockRotationStage'); >+ >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'Basic object tests' => sub { >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ }, >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ isa_ok( >+ $srstage, >+ 'Koha::StockRotationStage', >+ "Correctly create and load a stock rotation stage." >+ ); >+ >+ # Relationship to library >+ isa_ok( $srstage->branchcode, 'Koha::Library', "Fetched related branch." ); >+ is( $srstage->branchcode->branchcode, $library->{branchcode}, "Related branch OK." ); >+ >+ # Relationship to rota >+ isa_ok( $srstage->rota, 'Koha::StockRotationRota', "Fetched related rota." ); >+ is( $srstage->rota->rota_id, $rota->{rota_id}, "Related rota OK." ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'DBIx::Class::Ordered tests' => sub { >+ plan tests => 33; >+ >+ $schema->storage->txn_begin; >+ >+ my $library = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stagefirst = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 1 } >+ }); >+ my $stageprevious = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 2 } >+ }); >+ my $stage = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 3 }, >+ }); >+ my $stagenext = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 4 } >+ }); >+ my $stagelast = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { rota_id => $rota->{rota_id}, position => 5 } >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ >+ is($srstage->siblings->count, 4, "Siblings works."); >+ is($srstage->previous_siblings->count, 2, "Previous Siblings works."); >+ is($srstage->next_siblings->count, 2, "Next Siblings works."); >+ >+ my $map = { >+ first_sibling => $stagefirst, >+ previous_sibling => $stageprevious, >+ next_sibling => $stagenext, >+ last_sibling => $stagelast, >+ }; >+ # Test plain relations: >+ while ( my ( $srxsr, $check ) = each %{$map} ) { >+ my $sr = $srstage->$srxsr; >+ isa_ok($sr, 'Koha::StockRotationStage', "Fetched using '$srxsr'."); >+ is($sr->stage_id, $check->{stage_id}, "'$srxsr' data is correct."); >+ }; >+ >+ # Test mutators >+ ## Move Previous >+ ok($srstage->move_previous, "Previous."); >+ is($srstage->previous_sibling->stage_id, $stagefirst->{stage_id}, "Previous, correct previous."); >+ is($srstage->next_sibling->stage_id, $stageprevious->{stage_id}, "Previous, correct next."); >+ ## Move Next >+ ok($srstage->move_next, "Back to middle."); >+ is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Middle, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Middle, correct next."); >+ ## Move First >+ ok($srstage->move_first, "First."); >+ is($srstage->previous_sibling, 0, "First, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagefirst->{stage_id}, "First, correct next."); >+ ## Move Last >+ ok($srstage->move_last, "Last."); >+ is($srstage->previous_sibling->stage_id, $stagelast->{stage_id}, "Last, correct previous."); >+ is($srstage->next_sibling, 0, "Last, correct next."); >+ ## Move To >+ >+ ### Out of range moves. >+ is( >+ $srstage->move_to($srstage->siblings->count + 2), >+ 0, "Move above count of stages." >+ ); >+ is($srstage->move_to(0), 0, "Move to 0th position."); >+ is($srstage->move_to(-1), 0, "Move to negative position."); >+ >+ ### Move To >+ ok($srstage->move_to(3), "Move."); >+ is($srstage->previous_sibling->stage_id, $stageprevious->{stage_id}, "Move, correct previous."); >+ is($srstage->next_sibling->stage_id, $stagenext->{stage_id}, "Move, correct next."); >+ >+ # Group manipulation >+ my $newrota = $builder->build({ source => 'Stockrotationrota' }); >+ ok($srstage->move_to_group($newrota->{rota_id}), "Move to Group."); >+ is(Koha::StockRotationStages->find($srstage->stage_id)->rota_id, $newrota->{rota_id}, "Moved correctly."); >+ >+ # Delete in ordered context >+ ok($srstage->delete, "Deleted OK."); >+ is( >+ Koha::StockRotationStages->find($stageprevious)->next_sibling->stage_id, >+ $stagenext->{stage_id}, >+ "Delete, correctly re-ordered." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'Relationship to stockrotationitems' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ my $stage = $builder->build({ source => 'Stockrotationstage' }); >+ >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ $builder->build({ >+ source => 'Stockrotationitem', >+ value => { stage_id => $stage->{stage_id} }, >+ }); >+ >+ my $srstage = Koha::StockRotationStages->find($stage->{stage_id}); >+ my $sritems = $srstage->stockrotationitems; >+ is( >+ $sritems->count, 3, >+ 'Correctly fetched stockrotationitems associated with this stage' >+ ); >+ >+ isa_ok( >+ $sritems->next, 'Koha::StockRotationItem', >+ "Relationship correctly creates Koha::Objects." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+ >+subtest 'Tests for investigate (singular)' => sub { >+ >+ plan tests => 3; >+ >+ # In this subtest series we will primarily be testing whether items end up >+ # in the correct 'branched' section of the stage-report. We don't care >+ # for item reasons here, as they are tested in StockRotationItems. >+ >+ # We will run tests on first on an empty report (the base-case) and then >+ # on a populated report. >+ >+ # We will need: >+ # - Libraries which will hold the Items >+ # - Rota Which containing the related stages >+ # + Stages on which we run investigate >+ # * Items on the stages >+ >+ $schema->storage->txn_begin; >+ >+ # Libraries >+ my $library1 = $builder->build({ source => 'Branch' }); >+ my $library2 = $builder->build({ source => 'Branch' }); >+ my $library3 = $builder->build({ source => 'Branch' }); >+ >+ my $stage1lib = $builder->build({ source => 'Branch' }); >+ my $stage2lib = $builder->build({ source => 'Branch' }); >+ my $stage3lib = $builder->build({ source => 'Branch' }); >+ my $stage4lib = $builder->build({ source => 'Branch' }); >+ >+ my $libraries = [ $library1, $library2, $library3, $stage1lib, $stage2lib, >+ $stage3lib, $stage4lib ]; >+ >+ # Rota >+ my $rota = $builder->build({ >+ source => 'Stockrotationrota', >+ value => { cyclical => 0 }, >+ }); >+ >+ # Stages >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage1lib->{branchcode}, >+ duration => 10, >+ position => 1, >+ }, >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage2lib->{branchcode}, >+ duration => 20, >+ position => 2, >+ }, >+ }); >+ my $stage3 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage3lib->{branchcode}, >+ duration => 10, >+ position => 3, >+ }, >+ }); >+ my $stage4 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ rota_id => $rota->{rota_id}, >+ branchcode_id => $stage4lib->{branchcode}, >+ duration => 20, >+ position => 4, >+ }, >+ }); >+ >+ # Test on an empty report. >+ my $spec = { >+ $library1->{branchcode} => 1, >+ $library2->{branchcode} => 1, >+ $library3->{branchcode} => 1, >+ $stage1lib->{branchcode} => 2, >+ $stage2lib->{branchcode} => 1, >+ $stage3lib->{branchcode} => 3, >+ $stage4lib->{branchcode} => 4 >+ }; >+ while ( my ( $code, $count ) = each %{$spec} ) { >+ my $cnt = 0; >+ while ( $cnt < $count ) { >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ stage_id => $stage1->{stage_id}, >+ indemand => 0, >+ fresh => 1, >+ } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($item); >+ $dbitem->itemnumber->homebranch($code) >+ ->holdingbranch($code)->store; >+ $cnt++; >+ } >+ } >+ my $report = Koha::StockRotationStages >+ ->find($stage1->{stage_id})->investigate; >+ my $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ >+ # Items assigned to stag1lib -> log, hence $results[4] = 0; >+ is_deeply( $results, [ 1, 1, 1, 2, 1, 3, 4 ], "Empty report test 1."); >+ >+ # Now we test by adding the next stage's items to the same report. >+ $spec = { >+ $library1->{branchcode} => 3, >+ $library2->{branchcode} => 2, >+ $library3->{branchcode} => 1, >+ $stage1lib->{branchcode} => 4, >+ $stage2lib->{branchcode} => 2, >+ $stage3lib->{branchcode} => 0, >+ $stage4lib->{branchcode} => 3 >+ }; >+ while ( my ( $code, $count ) = each %{$spec} ) { >+ my $cnt = 0; >+ while ( $cnt < $count ) { >+ my $item = $builder->build({ >+ source => 'Stockrotationitem', >+ value => { >+ stage_id => $stage2->{stage_id}, >+ indemand => 0, >+ fresh => 1, >+ } >+ }); >+ my $dbitem = Koha::StockRotationItems->find($item); >+ $dbitem->itemnumber->homebranch($code) >+ ->holdingbranch($code)->store; >+ $cnt++; >+ } >+ } >+ >+ $report = Koha::StockRotationStages >+ ->find($stage2->{stage_id})->investigate($report); >+ $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ is_deeply( $results, [ 4, 3, 2, 6, 3, 3, 7 ], "full report test."); >+ >+ # Carry out db updates >+ foreach my $item (@{$report->{items}}) { >+ my $reason = $item->{reason}; >+ if ( $reason eq 'repatriation' ) { >+ $item->{object}->repatriate; >+ } elsif ( grep { $reason eq $_ } >+ qw/in-demand advancement initiation/ ) { >+ $item->{object}->advance; >+ } >+ } >+ >+ $report = Koha::StockRotationStages >+ ->find($stage1->{stage_id})->investigate; >+ $results = []; >+ foreach my $lib ( @{$libraries} ) { >+ my $items = $report->{branched}->{$lib->{branchcode}}->{items} || []; >+ push @{$results}, >+ scalar @{$items}; >+ } >+ # All items have been 'initiated', which means they are either happily in >+ # transit or happily at the library they are supposed to be. Either way >+ # they will register as 'not-ready' in the stock rotation report. >+ is_deeply( $results, [ 0, 0, 0, 0, 0, 0, 0 ], "All items now in logs."); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/api/v1/stockrotationstage.t b/t/db_dependent/api/v1/stockrotationstage.t >new file mode 100644 >index 0000000000..c76e9b1891 >--- /dev/null >+++ b/t/db_dependent/api/v1/stockrotationstage.t >@@ -0,0 +1,172 @@ >+#!/usr/bin/env perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Test::More tests => 1; >+use Test::Mojo; >+use Test::Warn; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+use C4::Auth; >+use Koha::StockRotationStages; >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling >+# this affects the other REST api tests >+t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); >+ >+my $remote_address = '127.0.0.1'; >+my $t = Test::Mojo->new('Koha::REST::V1'); >+ >+subtest 'move() tests' => sub { >+ >+ plan tests => 16; >+ >+ $schema->storage->txn_begin; >+ >+ my ( $unauthorized_borrowernumber, $unauthorized_session_id ) = >+ create_user_and_session( { authorized => 0 } ); >+ my ( $authorized_borrowernumber, $authorized_session_id ) = >+ create_user_and_session( { authorized => 1 } ); >+ >+ my $library1 = $builder->build({ source => 'Branch' }); >+ my $library2 = $builder->build({ source => 'Branch' }); >+ my $rota = $builder->build({ source => 'Stockrotationrota' }); >+ my $stage1 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library1->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ } >+ }); >+ my $stage2 = $builder->build({ >+ source => 'Stockrotationstage', >+ value => { >+ branchcode_id => $library2->{branchcode}, >+ rota_id => $rota->{rota_id}, >+ } >+ }); >+ my $rota_id = $rota->{rota_id}; >+ my $stage1_id = $stage1->{stage_id}; >+ >+ # Unauthorized attempt to update >+ my $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $unauthorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(403); >+ >+ # Invalid attempt to move a stage on a non-existant rota >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/99999999/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(404) >+ ->json_is( '/error' => "Not found - Invalid rota or stage ID" ); >+ >+ # Invalid attempt to move an non-existant stage >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/999999999/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(404) >+ ->json_is( '/error' => "Not found - Invalid rota or stage ID" ); >+ >+ # Invalid attempt to move stage to current position >+ my $curr_position = $stage1->{position}; >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => $curr_position >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(400) >+ ->json_is( '/error' => "Bad request - new position invalid" ); >+ >+ # Invalid attempt to move stage to invalid position >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 99999999 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(400) >+ ->json_is( '/error' => "Bad request - new position invalid" ); >+ >+ # Valid, authorised move >+ $tx = $t->ua->build_tx( >+ PUT => "/api/v1/rotas/$rota_id/stages/$stage1_id/position" => >+ json => 2 >+ ); >+ $tx->req->cookies( >+ { name => 'CGISESSID', value => $authorized_session_id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ $t->request_ok($tx)->status_is(200); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+sub create_user_and_session { >+ >+ my $args = shift; >+ my $flags = ( $args->{authorized} ) ? $args->{authorized} : 0; >+ my $dbh = C4::Context->dbh; >+ >+ my $user = $builder->build( >+ { >+ source => 'Borrower', >+ value => { >+ flags => $flags >+ } >+ } >+ ); >+ >+ # Create a session for the authorized user >+ my $session = C4::Auth::get_session(''); >+ $session->param( 'number', $user->{borrowernumber} ); >+ $session->param( 'id', $user->{userid} ); >+ $session->param( 'ip', '127.0.0.1' ); >+ $session->param( 'lasttime', time() ); >+ $session->flush; >+ >+ if ( $args->{authorized} ) { >+ $dbh->do( " >+ INSERT INTO user_permissions (borrowernumber,module_bit,code) >+ VALUES (?,3,'parameters_remaining_permissions')", undef, >+ $user->{borrowernumber} ); >+ } >+ >+ return ( $user->{borrowernumber}, $session->id ); >+} >+ >+1; >diff --git a/tools/stockrotation.pl b/tools/stockrotation.pl >new file mode 100755 >index 0000000000..a5a064a334 >--- /dev/null >+++ b/tools/stockrotation.pl >@@ -0,0 +1,531 @@ >+#!/usr/bin/perl >+ >+# Copyright 2016 PTFS-Europe Ltd >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 3 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+=head1 stockrotation.pl >+ >+ Script to handle stockrotation. Including rotas, their associated stages >+ and items >+ >+=cut >+ >+use Modern::Perl; >+use CGI; >+ >+use C4::Auth; >+use C4::Context; >+use C4::Output; >+ >+use Koha::Libraries; >+use Koha::StockRotationRotas; >+use Koha::StockRotationItems; >+use Koha::StockRotationStages; >+use Koha::Item; >+use Koha::Util::StockRotation qw(:ALL); >+ >+my $input = new CGI; >+ >+unless (C4::Context->preference('StockRotation')) { >+ # redirect to Intranet home if self-check is not enabled >+ print $input->redirect("/cgi-bin/koha/mainpage.pl"); >+ exit; >+} >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'tools/stockrotation.tt', >+ query => $input, >+ type => 'intranet', >+ flagsrequired => { >+ tools => '*', >+ stockrotation => '*', >+ }, >+ authnotrequired => 0 >+ } >+); >+ >+# Grab all passed data >+# 'our' since Plack changes the scoping >+# of 'my' >+our %params = $input->Vars(); >+ >+my $op = $params{op}; >+ >+if (!defined $op) { >+ >+ # No operation is supplied, we're just displaying the list of rotas >+ my $rotas = Koha::StockRotationRotas->search( >+ undef, >+ { >+ order_by => { -asc => 'title' } >+ } >+ )->as_list; >+ >+ $template->param( >+ existing_rotas => $rotas, >+ no_op_set => 1 >+ ); >+ >+} elsif ($op eq 'create_edit_rota') { >+ >+ # Edit an existing rota or define a new one >+ my $rota_id = $params{rota_id}; >+ >+ my $rota = {}; >+ >+ if (!defined $rota_id) { >+ >+ # No ID supplied, we're creating a new rota >+ # Create a shell rota hashref >+ $rota = { >+ cyclical => 1 >+ }; >+ >+ } else { >+ >+ # ID supplied, we're editing an existing rota >+ $rota = Koha::StockRotationRotas->find($rota_id); >+ >+ } >+ >+ $template->param( >+ rota => $rota, >+ op => $op >+ ); >+ >+} elsif ($op eq 'toggle_rota') { >+ >+ # Find and update the active status of the rota >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ my $new_active = ($rota->active == 1) ? 0 : 1; >+ >+ $rota->active($new_active)->store; >+ >+ # Return to rotas page >+ print $input->redirect('stockrotation.pl'); >+ >+} elsif ($op eq 'process_rota') { >+ >+ # Get a hashref of the submitted rota data >+ my $rota = get_rota_from_form(); >+ >+ if (!process_rota($rota)) { >+ >+ # The submitted rota was invalid >+ $template->param( >+ error => 'invalid_form', >+ rota => $rota, >+ op => 'create_edit_rota' >+ ); >+ >+ } else { >+ >+ # All was well, return to the rotas list >+ print $input->redirect('stockrotation.pl'); >+ >+ } >+ >+} elsif ($op eq 'manage_stages') { >+ >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ $template->param( >+ rota => $rota, >+ branches => get_branches(), >+ existing_stages => get_stages($rota), >+ rota_id => $params{rota_id}, >+ op => $op >+ ); >+ >+} elsif ($op eq 'create_edit_stage') { >+ >+ # Edit an existing stage or define a new one >+ my $stage_id = $params{stage_id}; >+ >+ my $rota_id = $params{rota_id}; >+ >+ if (!defined $stage_id) { >+ >+ # No ID supplied, we're creating a new stage >+ $template->param( >+ branches => get_branches(), >+ stage => {}, >+ rota_id => $rota_id, >+ op => $op >+ ); >+ >+ } else { >+ >+ # ID supplied, we're editing an existing stage >+ my $stage = Koha::StockRotationStages->find($stage_id); >+ >+ $template->param( >+ branches => get_branches(), >+ stage => $stage, >+ rota_id => $stage->rota->rota_id, >+ op => $op >+ ); >+ >+ } >+ >+} elsif ($op eq 'confirm_remove_from_rota') { >+ >+ # Get the stage we're deleting >+ $template->param( >+ op => $op, >+ rota_id => $params{rota_id}, >+ stage_id => $params{stage_id}, >+ item_id => $params{item_id} >+ ); >+ >+} elsif ($op eq 'confirm_delete_stage') { >+ >+ # Get the stage we're deleting >+ my $stage = Koha::StockRotationStages->find($params{stage_id}); >+ >+ $template->param( >+ op => $op, >+ stage => $stage >+ ); >+ >+} elsif ($op eq 'delete_stage') { >+ >+ # Get the stage we're deleting >+ my $stage = Koha::StockRotationStages->find($params{stage_id}); >+ >+ # Get the ID of the rota with which this stage is associated >+ # (so we can return to the "Manage stages" page after deletion) >+ my $rota_id = $stage->rota->rota_id; >+ >+ $stage->delete; >+ >+ # Return to the stages list >+ print $input->redirect("?op=manage_stages&rota_id=$rota_id"); >+ >+} elsif ($op eq 'process_stage') { >+ >+ # Get a hashref of the submitted stage data >+ my $stage = get_stage_from_form(); >+ >+ # The rota we're managing >+ my $rota_id = $params{rota_id}; >+ >+ if (!process_stage($stage, $rota_id)) { >+ >+ # The submitted stage was invalid >+ # Get all branches >+ my $branches = get_branches(); >+ >+ $template->param( >+ error => 'invalid_form', >+ all_branches => $branches, >+ stage => $stage, >+ rota_id => $rota_id, >+ op => 'create_edit_stage' >+ ); >+ >+ } else { >+ >+ # All was well, return to the stages list >+ print $input->redirect("?op=manage_stages&rota_id=$rota_id"); >+ >+ } >+ >+} elsif ($op eq 'manage_items') { >+ >+ my $rota = Koha::StockRotationRotas->find($params{rota_id}); >+ >+ # Get all items on this rota, for each prefetch their >+ # stage and biblio objects >+ my $items = Koha::StockRotationItems->search( >+ { 'stage.rota_id' => $params{rota_id} }, >+ { >+ prefetch => { >+ stage => { >+ 'stockrotationitems' => { >+ 'itemnumber' => 'biblionumber' >+ } >+ } >+ } >+ } >+ ); >+ >+ $template->param( >+ rota_id => $params{rota_id}, >+ error => $params{error}, >+ items => $items, >+ branches => get_branches(), >+ stages => get_stages($rota), >+ rota => $rota, >+ op => $op >+ ); >+ >+} elsif ($op eq 'move_to_next_stage') { >+ >+ move_to_next_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=" . $params{rota_id}); >+ >+} elsif ($op eq 'toggle_in_demand') { >+ >+ # Toggle the item's in_demand >+ toggle_indemand($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} elsif ($op eq 'remove_item_from_stage') { >+ >+ # Remove the item from the stage >+ remove_from_stage($params{item_id}, $params{stage_id}); >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} elsif ($op eq 'add_items_to_rota') { >+ >+ # The item's barcode, >+ # which we may or may not have been passed >+ my $barcode = $params{barcode}; >+ >+ # The rota we're adding the item to >+ my $rota_id = $params{rota_id}; >+ >+ # The uploaded file filehandle, >+ # which we may or may not have been passed >+ my $barcode_file = $input->upload("barcodefile"); >+ >+ # We need to create an array of one or more barcodes to >+ # insert >+ my @barcodes = (); >+ >+ # If the barcode input box was populated, use it >+ push @barcodes, $barcode if $barcode; >+ >+ # Only parse the uploaded file if necessary >+ if ($barcode_file) { >+ >+ # Call binmode on the filehandle as we want to set a >+ # UTF-8 layer on it >+ binmode($barcode_file, ":encoding(UTF-8)"); >+ # Parse the file into an array of barcodes >+ while (my $barcode = <$barcode_file>) { >+ $barcode =~ s/\r/\n/g; >+ $barcode =~ s/\n+/\n/g; >+ my @data = split(/\n/, $barcode); >+ push @barcodes, @data; >+ } >+ >+ } >+ >+ # A hashref to hold the status of each barcode >+ my $barcode_status = { >+ ok => [], >+ on_other => [], >+ on_this => [], >+ not_found => [] >+ }; >+ >+ # If we have something to work with, do it >+ get_barcodes_status($rota_id, \@barcodes, $barcode_status) if (@barcodes); >+ >+ # Now we know the status of each barcode, add those that >+ # need it >+ if (scalar @{$barcode_status->{ok}} > 0) { >+ >+ add_items_to_rota($rota_id, $barcode_status->{ok}); >+ >+ } >+ # If we were only passed one barcode and it was successfully >+ # added, redirect back to ourselves, we don't want to display >+ # a report, redirect also if we were passed no barcodes >+ if ( >+ scalar @barcodes == 0 || >+ (scalar @barcodes == 1 && scalar @{$barcode_status->{ok}} == 1) >+ ) { >+ >+ print $input->redirect("?op=manage_items&rota_id=$rota_id"); >+ >+ } else { >+ >+ # Report on the outcome >+ $template->param( >+ barcode_status => $barcode_status, >+ rota_id => $rota_id, >+ op => $op >+ ); >+ >+ } >+ >+} elsif ($op eq 'move_items_to_rota') { >+ >+ # The barcodes of the items we're moving >+ my @move = $input->param('move_item'); >+ >+ foreach my $item(@move) { >+ >+ # The item we're moving >+ my $item = Koha::Items->find($item); >+ >+ # Move it to the new rota >+ $item->add_to_rota($params{rota_id}); >+ >+ } >+ >+ # Return to the items list >+ print $input->redirect("?op=manage_items&rota_id=".$params{rota_id}); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >+ >+sub get_rota_from_form { >+ >+ return { >+ id => $params{id}, >+ title => $params{title}, >+ cyclical => $params{cyclical}, >+ description => $params{description} >+ }; >+} >+ >+sub get_stage_from_form { >+ >+ return { >+ stage_id => $params{stage_id}, >+ branchcode => $params{branchcode}, >+ duration => $params{duration} >+ }; >+} >+ >+sub process_rota { >+ >+ my $sub_rota = shift; >+ >+ # Fields we require >+ my @required = ('title','cyclical'); >+ >+ # Count of the number of required fields we have >+ my $valid = 0; >+ >+ # Ensure we have everything we require >+ foreach my $req(@required) { >+ >+ if (exists $sub_rota->{$req}) { >+ >+ chomp(my $value = $sub_rota->{$req}); >+ if (length $value > 0) { >+ $valid++; >+ } >+ >+ } >+ >+ } >+ >+ # If we don't have everything we need >+ return 0 if $valid != scalar @required; >+ >+ # Passed validation >+ # Find the rota we're updating >+ my $rota = Koha::StockRotationRotas->find($sub_rota->{id}); >+ >+ if ($rota) { >+ >+ $rota->title( >+ $sub_rota->{title} >+ )->cyclical( >+ $sub_rota->{cyclical} >+ )->description( >+ $sub_rota->{description} >+ )->store; >+ >+ } else { >+ >+ $rota = Koha::StockRotationRota->new({ >+ title => $sub_rota->{title}, >+ cyclical => $sub_rota->{cyclical}, >+ active => 0, >+ description => $sub_rota->{description} >+ })->store; >+ >+ } >+ >+ return 1; >+} >+ >+sub process_stage { >+ >+ my ($sub_stage, $rota_id) = @_; >+ >+ # Fields we require >+ my @required = ('branchcode','duration'); >+ >+ # Count of the number of required fields we have >+ my $valid = 0; >+ >+ # Ensure we have everything we require >+ foreach my $req(@required) { >+ >+ if (exists $sub_stage->{$req}) { >+ >+ chomp(my $value = $sub_stage->{$req}); >+ if (length $value > 0) { >+ $valid++; >+ } >+ >+ } >+ >+ } >+ >+ # If we don't have everything we need >+ return 0 if $valid != scalar @required; >+ >+ # Passed validation >+ # Find the stage we're updating >+ my $stage = Koha::StockRotationStages->find($sub_stage->{stage_id}); >+ >+ if ($stage) { >+ >+ # Updating an existing stage >+ $stage->branchcode_id( >+ $sub_stage->{branchcode} >+ )->duration( >+ $sub_stage->{duration} >+ )->store; >+ >+ } else { >+ >+ # Creating a new stage >+ $stage = Koha::StockRotationStage->new({ >+ branchcode_id => $sub_stage->{branchcode}, >+ rota_id => $rota_id, >+ duration => $sub_stage->{duration} >+ })->store; >+ >+ } >+ >+ return 1; >+} >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >-- >2.18.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11897
:
25887
|
58207
|
58208
|
58209
|
58210
|
58211
|
58212
|
58213
|
58214
|
58215
|
58216
|
58217
|
58218
|
58219
|
58280
|
58281
|
58687
|
58692
|
58693
|
58694
|
58695
|
58696
|
60175
|
60715
|
61825
|
61826
|
61827
|
61828
|
61829
|
61830
|
61831
|
61832
|
61833
|
61834
|
61835
|
61836
|
61837
|
61838
|
61839
|
61885
|
62730
|
62750
|
62751
|
62752
|
62753
|
62754
|
62755
|
62756
|
62757
|
62758
|
62759
|
62760
|
62761
|
62762
|
62763
|
62764
|
62765
|
62766
|
62767
|
63063
|
63064
|
63065
|
63066
|
63067
|
63068
|
63069
|
63070
|
63071
|
63072
|
63073
|
63074
|
63075
|
63076
|
63077
|
63078
|
63079
|
63080
|
63114
|
64850
|
64851
|
65715
|
65716
|
65717
|
65718
|
65719
|
65720
|
65721
|
65722
|
65723
|
65724
|
65725
|
65726
|
65727
|
65728
|
65729
|
65730
|
65731
|
65732
|
65733
|
65734
|
65735
|
65736
|
65737
|
65738
|
65739
|
65740
|
65741
|
65742
|
65743
|
65744
|
65745
|
65746
|
65747
|
65748
|
65749
|
65750
|
65751
|
65752
|
65753
|
65754
|
65755
|
65756
|
65757
|
65758
|
66381
|
70496
|
70497
|
70498
|
70499
|
70500
|
70501
|
70502
|
70503
|
70504
|
70505
|
70506
|
70507
|
70508
|
70509
|
70510
|
70511
|
70512
|
70513
|
70514
|
70515
|
70516
|
70517
|
70518
|
70519
|
70520
|
70521
|
70522
|
70523
|
70524
|
72124
|
72125
|
72126
|
72127
|
72128
|
74003
|
74004
|
74005
|
74006
|
74007
|
74008
|
74010
|
74011
|
74012
|
74013
|
74014
|
74015
|
74016
|
74017
|
74197
|
74198
|
74199
|
74200
|
74201
|
74202
|
74203
|
74204
|
74205
|
74206
|
74319
|
74320
|
74360
|
74459
|
74460
|
74461
|
74462
|
74463
|
74464
|
74465
|
74466
|
74467
|
74468
|
74469
|
74470
|
75409
|
75410
|
75411
|
75412
|
75413
|
75414
|
75415
|
75416
|
75417
|
75418
|
75419
|
75420
|
76121
|
76122
|
76123
|
76124
|
76125
|
76126
|
76127
|
76128
|
76129
|
76130
|
76131
|
76132
|
76133
|
76134
|
76135
|
76144
|
76706
|
76707
|
76708
|
76709
|
76710
|
77623
|
77624
|
77625
|
77626
|
78387
|
78388
|
78389
|
78390
|
78391
|
78392
|
78393
|
78394
|
78404
|
78405
|
78406
|
78407
|
78408
|
79040
|
79041
|
79042
|
79043
|
79044
|
79738
|
79739
|
79740
|
79741
|
79742
|
79746
|
79747
|
79748
|
79749
|
79750
|
79964
|
79965
|
79966
|
79967
|
79968
|
79969
|
79970
|
79971
|
79972
|
79973
|
79974
|
79975
|
80068
|
80087
|
80088
|
80089
|
80090
|
80091
|
80092
|
80093
|
80094
|
80095
|
80096
|
80097
|
80098
|
80099
|
80100
|
80118
|
80260
|
80261
|
80262
|
80263
|
80264
|
80265
|
80266
|
80267
|
80268
|
80269
|
80270
|
80271
|
80272
|
80273
|
80274
|
80275
|
80289
|
80298
|
80299
|
80300