From df8fd1972dfb41cf6bb1f9b3b56b6ee6b0725c14 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: Add bulk of Stockrotation module.

This commit replaces a set of about 20 commits into one large
commit.  This makes working with the module way easier from bugzilla.

As it stands the patches on this bug provide a fully functioning stock
rotation feature set for Koha.

All previous patches that added functionality were signed off, so I'm
retaining that flag.

Signed-off-by: Kathleen Milne <kathleen.milne@cne-siar.gov.uk>
---
 Koha/Item.pm                                       |  49 +-
 Koha/Library.pm                                    |  46 ++
 Koha/REST/V1/Stage.pm                              |  48 ++
 Koha/StockRotationItem.pm                          | 273 +++++++++++
 Koha/StockRotationItems.pm                         | 124 +++++
 Koha/StockRotationRota.pm                          | 182 +++++++
 Koha/StockRotationRotas.pm                         | 101 ++++
 Koha/StockRotationStage.pm                         | 413 ++++++++++++++++
 Koha/StockRotationStages.pm                        |  86 ++++
 Koha/Util/StockRotation.pm                         | 247 ++++++++++
 api/v1/swagger/paths.json                          |   3 +
 api/v1/swagger/paths/rotas.json                    |  79 ++++
 catalogue/stockrotation.pl                         | 175 +++++++
 koha-tmpl/intranet-tmpl/prog/css/staff-global.css  | 128 +++++
 .../prog/en/includes/biblio-view-menu.inc          |   1 +
 .../prog/en/includes/stockrotation-toolbar.inc     |  13 +
 .../intranet-tmpl/prog/en/includes/tools-menu.inc  |  22 +
 .../en/modules/admin/preferences/circulation.pref  |   2 +-
 .../prog/en/modules/catalogue/stockrotation.tt     | 157 ++++++
 .../prog/en/modules/tools/stockrotation.tt         | 482 +++++++++++++++++++
 .../prog/en/modules/tools/tools-home.tt            |   7 +
 .../intranet-tmpl/prog/js/pages/stockrotation.js   |  65 +++
 misc/cronjobs/stockrotation.pl                     | 489 +++++++++++++++++++
 t/db_dependent/Items.t                             |  61 +++
 t/db_dependent/Koha/Libraries.t                    |  28 +-
 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                             | 526 +++++++++++++++++++++
 30 files changed, 4921 insertions(+), 3 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 bea05f8317..cba8d5adf6 100644
--- a/Koha/Item.pm
+++ b/Koha/Item.pm
@@ -30,6 +30,8 @@ use Koha::IssuingRules;
 use Koha::Item::Transfer;
 use Koha::Patrons;
 use Koha::Libraries;
+use Koha::StockRotationItem;
+use Koha::StockRotationRotas;
 
 use base qw(Koha::Object);
 
@@ -235,7 +237,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..fa4ca7ad2b
--- /dev/null
+++ b/Koha/REST/V1/Stage.pm
@@ -0,0 +1,48 @@
+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;
+
+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..8fde691ae4
--- /dev/null
+++ b/Koha/StockRotationItems.pm
@@ -0,0 +1,124 @@
+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';
+}
+
+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..c308a9246b
--- /dev/null
+++ b/Koha/StockRotationRotas.pm
@@ -0,0 +1,101 @@
+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';
+}
+
+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..eadb71686e
--- /dev/null
+++ b/Koha/StockRotationStage.pm
@@ -0,0 +1,413 @@
+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->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 => $branch->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 => $branch->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..00faaab28d
--- /dev/null
+++ b/Koha/StockRotationStages.pm
@@ -0,0 +1,86 @@
+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';
+}
+
+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 f3e843e6c3..c210c90959 100644
--- a/api/v1/swagger/paths.json
+++ b/api/v1/swagger/paths.json
@@ -25,5 +25,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..ff63b8a270
--- /dev/null
+++ b/catalogue/stockrotation.pl
@@ -0,0 +1,175 @@
+#!/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 }
+    }
+);
+
+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
+        }
+    );
+
+    # 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/koha-tmpl/intranet-tmpl/prog/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
index 32a614bd70..c4ba0262f7 100644
--- a/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
+++ b/koha-tmpl/intranet-tmpl/prog/css/staff-global.css
@@ -3218,3 +3218,131 @@ span.name {
     font-weight: bold;
     font-style: italic;
 }
+
+#stockrotation h3 {
+    margin: 30px 0 10px 0;
+}
+
+#stockrotation .dialog h3 {
+    margin: 10px 0;
+}
+
+#stockrotation .dialog {
+    margin-bottom: 20px;
+}
+
+#stockrotation .highlight_stage,
+#catalog_stockrotation .highlight_stage {
+    font-weight: bold;
+}
+
+#stockrotation #rota_form textarea {
+    width: 300px;
+    height: 100px;
+}
+
+#stockrotation #rota_form #name {
+    width: 300px;
+}
+
+#stockrotation #rota_form fieldset,
+#stockrotation #stage_form fieldset,
+#stockrotation #add_rota_item_form fieldset {
+    width: auto;
+}
+
+#stockrotation .dialog.alert ul {
+    margin: 20px 0;
+}
+
+#stockrotation .dialog.alert li {
+    list-style-type: none;
+}
+
+#catalog_stockrotation .item_select_rota {
+    vertical-align: middle;
+}
+
+#catalog_stockrotation 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;
+}
+
+#stage_list_headings span {
+    padding: 3px;
+}
+
+#manage_stages ul {
+    padding-left: 0;
+}
+
+#manage_stages li {
+    list-style: none;
+    margin-bottom: 5px;
+}
+
+#manage_stages li span {
+    padding: 6px 3px;
+}
+
+#manage_stages .stagename {
+    width: 15em;
+    display: inline-block;
+}
+
+#manage_stages .stageduration {
+    width: 10em;
+    display: inline-block;
+}
+
+#manage_stages .stageactions {
+    display: inline-block;
+}
+
+#manage_stages li:nth-child(odd) {
+    background-color : #F3F3F3;
+}
+
+#manage_stages .drag_handle {
+    margin-right: 6px;
+    cursor: move;
+}
+
+#manage_stages .drag_placeholder {
+    height: 2em;
+    border: 1px dotted #aaa;
+}
+
+#manage_stages h3 {
+    display: inline-block;
+}
+
+#manage_stages #ajax_status {
+    display: inline-block;
+    border: 1px solid #bcbcbc;
+    border-radius: 5px;
+    padding: 5px;
+    margin-left: 10px;
+    background: #f3f3f3;
+}
+
+#manage_stages #manage_stages_help {
+    margin: 20px 0;
+}
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 95c0ee4640..b32f60939e 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,6 +40,7 @@
 [% 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&amp;modules=CATALOGUING&amp;action=MODIFY&amp;object=[% biblio_object_id | url  %]">Modification log</a> </li>[% END %]
+[% IF ( CAN_user_stockrotation_can_add_items_rotas && 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/stockrotation-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc
new file mode 100644
index 0000000000..c8e72e9dd6
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/stockrotation-toolbar.inc
@@ -0,0 +1,13 @@
+[% 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="addstage" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_stage&amp;rota_id=[% rota_id %]"><i class="fa fa-plus"></i> New stage</a>
+    [% END %]
+    [% IF op == 'manage_items' %]
+        <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>
+        <a id="editrota" class="btn btn-default btn-sm" href="/cgi-bin/koha/tools/stockrotation.pl?op=create_edit_rota&amp;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 50e52f73c0..d4a6ad2b1c 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,22 @@
+[% USE Koha %]
+
+<script type="text/javascript">//<![CDATA[
+    $(document).ready(function() {
+        var path = location.pathname.substring(1);
+        if (path.indexOf("labels") >= 0 && path.indexOf("spine") < 0 ) {
+          $('#navmenulist a[href$="/cgi-bin/koha/labels/label-home.pl"]').css('font-weight','bold');
+        } else if (path.indexOf("patroncards") >= 0 ) {
+          $('#navmenulist a[href$="/cgi-bin/koha/patroncards/home.pl"]').css('font-weight','bold');
+        } else if (path.indexOf("patron_lists") >= 0 ) {
+          $('#navmenulist a[href$="/cgi-bin/koha/patron_lists/lists.pl"]').css('font-weight','bold');
+        } else if ((path+location.search).indexOf("batchMod.pl?del=1") >= 0 ) {
+          $('#navmenulist a[href$="/cgi-bin/koha/tools/batchMod.pl?del=1"]').css('font-weight','bold');
+        } else {
+          $('#navmenulist a[href$="/' + path + '"]').css('font-weight','bold');
+        }
+    });
+//]]>
+</script>
 <div id="navmenu">
 <div id="navmenulist">
 <ul>
@@ -38,6 +57,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_can_edit_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/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
index aba5c0ab0a..ca79155d74 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref
@@ -901,7 +901,7 @@ Circulation:
                   yes: Enable
                   no: Disable
             - "housebound module"
-    Stockrotation module:
+    Stock Rotation module:
         -
             - pref: StockRotation
               choices:
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..8d9b011d87
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/stockrotation.tt
@@ -0,0 +1,157 @@
+[% USE Koha %]
+[% USE Branches %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Catalog &rsaquo; Stock rotation details for [% biblio.title %]</title>
+[% INCLUDE 'doc-head-close.inc' %]
+[% INCLUDE 'browser-strings.inc' %]
+<!--[if lt IE 9]>
+<script type="text/javascript" src="[% interface %]/lib/shims/json2.min.js"></script>
+<![endif]-->
+<script type="text/javascript" src="[% interface %]/js/browser.js"></script>
+<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> &rsaquo; <a href="/cgi-bin/koha/catalogue/search.pl">Catalog</a>  &rsaquo; 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>Shelfmark</th>
+                        <th>Rota</th>
+                        <th>Rota status</th>
+                        <th>In transit</th>
+                        <th>Stages &amp; duration in days<br>(current stage highlighted)</th>
+                        <th>&nbsp;</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 %]
+                                        [% item.rota.active ? 'Active' : 'Inactive' %]
+
+                                    [% IF !item.rota.active %]
+                                        </span>
+                                    [% END %]
+                                [% END %]
+                            </td>
+                            <td>
+                                [% item.bib_item.get_transfer ? 'Yes' : 'No' %]
+                            </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>
+                                    &raquo;
+                                [% END %]
+                                [% IF item.stages.size > 0 %]
+                                    <span class="stage">[% item.rota.cyclical ? 'START' : '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&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;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&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;biblionumber=[% biblionumber %]">
+                                    [% ELSE %]
+                                        <a class="btn btn-default btn-xs" disabled>
+                                    [% END %]
+                                        <i class="fa fa-fire"></i>
+                                        [% item.stockrotationitem.indemand ? 'Remove &quot;In demand&quot;' : 'Add &quot;In demand&quot;' %]
+                                    </a>
+                                    [% IF !in_transit %]
+                                        <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&amp;stage_id=[% item.stockrotationitem.stage.stage_id %]&amp;item_id=[% item.bib_item.id %]&amp;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&amp;stage_id=[% stage_id %]&amp;item_id=[% item_id %]&amp;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..e9cf827fad
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stockrotation.tt
@@ -0,0 +1,482 @@
+[% USE Koha %]
+[% USE Branches %]
+[% USE KohaDates %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Stock rotation</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/datatables.css" />
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript" src="[% interface %]/[% theme %]/js/pages/stockrotation.js"></script>
+<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>
+    &rsaquo; <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a>
+
+[% IF no_op_set %]
+    &rsaquo; Stock rotation
+[% ELSE %]
+    &rsaquo; <a href="/cgi-bin/koha/tools/stockrotation.pl">Stock rotation</a>
+[% END %]
+
+[% IF (op == 'create_edit_rota' && rota.rota_id) %]
+    &rsaquo; Edit rota
+[% ELSIF (op == 'create_edit_rota' && !rota.rota_id) %]
+    &rsaquo; Create rota
+[% ELSIF (op == 'manage_stages') %]
+    &rsaquo; Manage stages
+[% ELSIF (op == 'create_edit_stage' && stage.id) %]
+    <a href="?op=manage_stages&amp;rota_id=[% rota_id %]">&rsaquo; Manage stages</a>
+    &rsaquo; Edit stage
+[% ELSIF (op == 'create_edit_stage' && !stage.id) %]
+    <a href="?op=manage_stages&amp;rota_id=[% rota_id %]">&rsaquo; Manage stages</a>
+    &rsaquo; Create stage
+[% ELSIF (op == 'manage_items') %]
+    &rsaquo; Manage items
+[% END %]
+
+</div>
+
+<div id="doc3" class="yui-t2">
+    <div id="bd">
+        <div id="yui-main">
+            <div id="stockrotation" class="yui-b">
+
+                <h1>Stock rotation</h1>
+
+                [% IF no_op_set %]
+
+                    [% INCLUDE 'stockrotation-toolbar.inc' %]
+
+                    [% IF existing_rotas.size > 0 %]
+                        <h3>Manage existing rotas</h3>
+                        <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>&nbsp;</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&amp;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&amp;rota_id=[% rota.rota_id %]">Stages</a></li>
+                                                    [% IF CAN_user_stockrotation_can_add_items_rotas && rota.stockrotationstages.count > 0 %]
+                                                    <li><a href="?op=manage_items&amp;rota_id=[% rota.rota_id %]">Items</a></li>
+                                                    [% END %]
+                                                </ul>
+                                            </div>
+                                            <a class="btn btn-default btn-xs" href="?op=toggle_rota&amp;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 %]
+                        <h3>Edit "[% rota.title %]"</h3>
+                    [% ELSE %]
+                        <h3>Create new rota</h3>
+                    [% 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="active">Active:</label>
+                                    <select name="active" id="active">
+                                        [% IF rota.active %]
+                                            <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 existing_stages.size > 0 %]
+                        <div id="manage_stages">
+                            <h3>Manage stages</h3>
+                            <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>
+                            <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">Branch</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&amp;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&amp;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 %]
+                        <h3>Edit "[% Branches.GetName(stage.branchcode_id) %]"</h3>
+                    [% ELSE %]
+                        <h3>Create new stage</h3>
+                    [% 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">Branch:</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&amp;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&amp;item_id=[% item_id %]&amp;stage_id=[% stage_id %]&amp;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&amp;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&amp;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&amp;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 %]
+
+                    <div>
+                        <form id="add_rota_item_form" method="post" enctype="multipart/form-data" class="validated">
+                            <fieldset class="rows">
+                                <legend>Add item to &quot;[% rota.title %]&quot;</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 &quot;[% rota.title %]&quot;</h3>
+                        <table id="sr_manage_items" class="items_table" role="grid">
+                            <thead>
+                                <th>Barcode</th>
+                                <th>Title</th>
+                                <th>Author</th>
+                                <th>Shelfmark</th>
+                                <th class="NoSearch">In transit</th>
+                                <th class="NoSort">Stages &amp; duration in days<br>(current stage highlighted)</th>
+                                <th class="NoSort">&nbsp;</th>
+                            </thead>
+                            <tbody>
+                                [% FOREACH item IN items %]
+                                    <tr>
+                                        <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% item.id %]&amp;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>
+                                                &raquo;
+                                            [% 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&amp;rota_id=[% rota.id %]&amp;item_id=[% item.id %]&amp;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&amp;stage_id=[% item.stage.stage_id %]&amp;item_id=[% item.id %]&amp;rota_id=[% rota.id %]">
+                                            [% ELSE %]
+                                                <a class="btn btn-default btn-xs" disabled>
+                                            [% END %]
+                                                <i class="fa fa-fire"></i>
+                                                [% item.indemand ? 'Remove &quot;In demand&quot;' : 'Add &quot;In demand&quot;' %]
+                                            </a>
+                                            [% IF !in_transit %]
+                                                <a class="btn btn-default btn-xs" href="?op=confirm_remove_from_rota&amp;stage_id=[% item.stage.stage_id %]&amp;item_id=[% item.id %]&amp;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 &quot;[% item_on_other.stockrotationitem.stage.rota.title %]&quot;)</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&amp;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 4c8cca9192..0ff70a27dd 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 &rsaquo; Tools</title>
 [% INCLUDE 'doc-head-close.inc' %]
@@ -68,6 +70,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_can_edit_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..d8914e42d9
--- /dev/null
+++ b/misc/cronjobs/stockrotation.pl
@@ -0,0 +1,489 @@
+#!/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 Mail::Sendmail;
+use C4::Context;
+use Koha::Email;
+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
+--------------------
+  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, {
+        body            => $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}, $branch->branchcode,
+            $branch->branchname
+        );
+    } elsif ( $data->{actionable} ) {     # Full email report
+        while ( my ($branchcode_id, $details) = each %{$branched} ) {
+            push @{$out}, _report_per_branch(
+                $details, $details->{code}, $details->{name}
+            ) 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 ( $per_branch, $branchcode, $branchname ) = @_;
+
+    my $out = "";
+    my $status = 0;
+
+    $out .= sprintf "\nStockrotation report for '%s' [%s]:\n",
+        $branchname || "N/A", $branchcode || "N/A";
+    if ( $per_branch && @{$per_branch->{items}} ) {
+        $out .= sprintf "
+  Email: %s
+  Phone: %s
+  Items:",
+        $per_branch->{email} || "N/A", $per_branch->{phone} || "N/A";
+        $status++;
+    } else {
+        $out .= sprintf "No items to be processed for this branch.\n\n";
+    }
+    $out .= join("", map {
+        _print_item($_) unless $_->{reason} eq 'in-demand'
+    } @{$per_branch->{items}});
+    return {
+        body          => $out,                 # The body of the report
+        email_address => $per_branch->{email}, # The branch email address
+        status        => $status,              # We may have empty report...
+    };
+}
+
+=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_email` and
+`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->{body}: the body of the report
+    #   - part->{email_address}: the email address to send the report to
+    my $report = $params->{report};
+    my $header = shift @{$report};
+    my $parts = $report;
+
+    foreach my $part (@{$parts}) {
+        # The final message is the header + body of this part.
+        my $msg = $header . $part->{body};
+        $msg .= "No database updates have been performed.\n\n"
+            unless ( $params->{execute} );
+
+        if ( $params->{send_email} ) { # Only email if emails requested
+            if ( $part->{status} || $params->{send_all} ) {
+                # We have a report to send, or we want to send even empty
+                # reports.
+                my $message = Koha::Email->new;
+
+                # Collate 'to' addresses
+                my @to = ();
+                if ( $part->{email_address} ) {
+                    push @to, $part->{email_address};
+                } elsif ( !$part->{no_branch_email} ) {
+                    $msg = "***We tried to send a branch report, but we have no email address for this branch.***\n\n" . $msg;
+                    push @to, C4::Context->preference('KohaAdminEmailAddress')
+                        if ( C4::Context->preference('KohaAdminEmailAddress') );
+                }
+                push @to, $params->{admin_email} if $params->{admin_email};
+
+                # Create email data.
+                my %mail = $message->create_message_headers(
+                    {
+                        to          => join("; ", @to),
+                        subject     => "Stockrotation Email Report",
+                        message     => Encode::encode("utf8", $msg),
+                        contenttype => 'text/plain; charset="utf8"',
+                    }
+                );
+
+                # Send or die.
+                die($Mail::Sendmail::error)
+                    unless Mail::Sendmail::sendmail(%mail);
+            }
+
+        } else {                       #  Else emit to stdout.
+            printf $msg;
+        }
+    }
+}
+
+#### Main Code
+
+# Compile Stockrotation Report data
+my $rotas = Koha::StockRotationRotas->search;
+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 ef098734c9..fcbc621ac9 100755
--- a/t/db_dependent/Items.t
+++ b/t/db_dependent/Items.t
@@ -833,6 +833,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..1d72f73442 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 => 12;
 
 use Koha::Library;
 use Koha::Libraries;
@@ -53,6 +53,32 @@ 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' );
 
+$retrieved_category_2->delete;
+is( Koha::LibraryCategories->search->count, $nb_of_categories + 2, 'Delete should have deleted the library category' );
+
+# 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..19f71ddee0
--- /dev/null
+++ b/tools/stockrotation.pl
@@ -0,0 +1,526 @@
+#!/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',
+        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->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,
+            active   => 0
+        };
+
+    } 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},
+        active      => $params{active},
+        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','active');
+
+    # 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}
+        )->active(
+            $sub_rota->{active}
+        )->description(
+            $sub_rota->{description}
+        )->store;
+
+    } else {
+
+        $rota = Koha::StockRotationRota->new({
+            title       => $sub_rota->{title},
+            cyclical    => $sub_rota->{cyclical},
+            active      => $sub_rota->{active},
+            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.14.2