From 910e5845306cb548be0efaea2bdc67fbd86760e8 Mon Sep 17 00:00:00 2001 From: Olli-Antti Kivilahti Date: Fri, 8 May 2015 15:40:42 +0300 Subject: [PATCH 1/3] Bug 9525: Group floating rules +-----------------------------+ | USAGE TIPS (sorry didn't document these in the GUI | +-----------------------------+ When modifying a floating route rule, pressing "%" accepts the modification and pressing "V" cancels the modifications and reverts the old rule. The empty text box in the center of the route rule (under the floating type dropdown list) is the box for conditional rules, where you put the logical expression. +-----------------------------+ | SETTING UP THE TEST CONTEXT | +-----------------------------+ -1. Find an Item, which can be circulated freely, and has homebranch B and any holdingbranch. 0. Read the instructions at admin/floating-matrix.pl. +-----------+ | TEST PLAN: Disabled floating: +-----------+ 0. Make sure that the floating rules from branch A to branch B (A -> B) are "Disabled" and vice versa (B -> A). 1. Set your current library to A. 2. Check-in the Item. 3. Observe the started transport to branch B. 4. Set your current library to B. 5. Check-in to receive the transfer. +-----------+ | TEST PLAN: Always floating: +-----------+ 0. Make sure that the floating rules from branch A to branch B (A -> B) are "Allowed". 1. Set your current library to A. 2. Check-in the Item. 3. Observe that there is not transport prompt to branch B and the Item is not "in-transit" -state. +-----------+ | TEST PLAN: Possibly floating: +-----------+ 0. Make sure that the floating rules from branch A to branch B (A -> B) are "Possible". 1. Set your current library to A. 2. Check-in the Item. 3. Observe that there is a transport prompt to branch B. You can decide if you want to transfer or no. +-----------+ | TEST PLAN: Conditional floating: +-----------+ 0. Same as Always floating if the given condition matches. See the feature description at admin/floating-matrix.pl explaining Conditional floating. +-----------+ | TEST PLAN: Floating rule tester: +-----------+ 0. Add some floating rules between libraries 1. Choose origin library, destination library and give barcode for the item you want to test 2. Hit "Test" 3. Make sure box next to the "Test" button changes color according rules you made 4. Also for conditional floating use barcode for item that doesn't match given condition --- C4/Circulation.pm | 29 +- Koha/FloatingMatrix.pm | 479 ++++++++++++++++++ Koha/FloatingMatrix/BranchRule.pm | 342 +++++++++++++ Koha/Schema/Result/Branch.pm | 34 +- Koha/Schema/Result/FloatingMatrix.pm | 145 ++++++ admin/floating-matrix-api.pl | 98 ++++ admin/floating-matrix.pl | 138 +++++ .../Bug-9525_group_floating_rules.perl | 26 + installer/data/mysql/kohastructure.sql | 18 + .../prog/css/floating-matrix-loader.css | 78 +++ .../prog/css/floating-matrix.css | 24 + .../admin/floating-matrix-floatingTypes.inc | 26 + .../prog/en/modules/admin/admin-home.tt | 2 + .../prog/en/modules/admin/floating-matrix.tt | 128 +++++ .../intranet-tmpl/prog/js/floating-matrix.js | 250 +++++++++ 15 files changed, 1810 insertions(+), 7 deletions(-) create mode 100644 Koha/FloatingMatrix.pm create mode 100644 Koha/FloatingMatrix/BranchRule.pm create mode 100644 Koha/Schema/Result/FloatingMatrix.pm create mode 100755 admin/floating-matrix-api.pl create mode 100755 admin/floating-matrix.pl create mode 100644 installer/data/mysql/atomicupdate/Bug-9525_group_floating_rules.perl create mode 100644 koha-tmpl/intranet-tmpl/prog/css/floating-matrix-loader.css create mode 100644 koha-tmpl/intranet-tmpl/prog/css/floating-matrix.css create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/admin/floating-matrix-floatingTypes.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/floating-matrix.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/js/floating-matrix.js diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 860d965cf1..8c6e717e39 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -37,6 +37,7 @@ use C4::Overdues qw(CalcFine UpdateFine get_chargeable_units); use C4::RotatingCollections qw(GetCollectionItemBranches); use Algorithm::CheckDigits; +use Koha::FloatingMatrix; use Data::Dumper; use Koha::Account; use Koha::AuthorisedValues; @@ -2130,21 +2131,39 @@ sub AddReturn { DelUniqueDebarment({ borrowernumber => $borrowernumber, type => 'OVERDUES' }); } + my $floatingType = Koha::FloatingMatrix::CheckFloating($item, $branch, $returnbranch); # Transfer to returnbranch if Automatic transfer set or append message NeedsTransfer - if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'}){ + if (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) and !$resfound and ($branch ne $returnbranch) and not $messages->{'WrongTransfer'} && (not($floatingType) || $floatingType eq 'POSSIBLE')){ my $BranchTransferLimitsType = C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode'; - if (C4::Context->preference("AutomaticItemReturn" ) or + if ( not($floatingType && $floatingType eq 'POSSIBLE') and + (C4::Context->preference("AutomaticItemReturn" ) or (C4::Context->preference("UseBranchTransferLimits") and - ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType ) - )) { + ! IsBranchTransferAllowed($branch, $returnbranch, $item->$BranchTransferLimitsType ))) + ) + { $debug and warn sprintf "about to call ModItemTransfer(%s, %s, %s)", $item->itemnumber,$branch, $returnbranch; - $debug and warn "item: " . Dumper($item_unblessed); + $debug and warn "item: " . Dumper($item); ModItemTransfer($item->itemnumber, $branch, $returnbranch); $messages->{'WasTransfered'} = 1; } else { $messages->{'NeedsTransfer'} = $returnbranch; } } + #This elsif-clause copypastes the upper if-clause. This is horrible and I cry, but cannot refactor this mess now :( due to Koha upstream master stuff looming. + elsif (!$is_in_rotating_collection && ($doreturn or $messages->{'NotIssued'}) + and !$resfound and ($branch ne $returnbranch) + and not($messages->{'WrongTransfer'}) + && $floatingType){ #So if we would otherwise transfer, but we have a floating rule overriding it. + #We can infer that the transfer was averted because of a floating rule. + #Make sure we dont log the same floating denial multiple times + #So first check if we already logged this operation. + my $logs = C4::Log::GetLogs(DateTime->now(time_zone => C4::Context->tz), + DateTime->now(time_zone => C4::Context->tz), + undef, ['FLOATING'], undef, $item->itemnumber, "$branch -> $returnbranch denied"); + if (not($logs) || (ref($logs) eq 'ARRAY' && scalar(@$logs) == 0)) { + C4::Log::logaction("FLOATING", $floatingType, $item->itemnumber, "$branch -> $returnbranch denied"); + } + } if ( C4::Context->preference('ClaimReturnedLostValue') ) { my $claims = Koha::Checkouts::ReturnClaims->search( diff --git a/Koha/FloatingMatrix.pm b/Koha/FloatingMatrix.pm new file mode 100644 index 0000000000..6bcf62c703 --- /dev/null +++ b/Koha/FloatingMatrix.pm @@ -0,0 +1,479 @@ +package Koha::FloatingMatrix; + +# Copyright 2015 Vaara-kirjastot +# +# 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 2 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 FloatingMatrix + +Koha::FloatingMatrix - Object to control accessing and modifying floating matrix rules. + +=cut + +use Modern::Perl; +use Carp qw(carp croak confess longmess); +use Scalar::Util qw(blessed refaddr); + +use C4::Context qw(dbh); +use Koha::Caches; +use Koha::Database; + +use Koha::FloatingMatrix::BranchRule; + +use Koha::Exceptions::BadParameter; + +my $cacheExpiryTime = 1200; + +=head new + + my $fm = Koha::FloatingMatrix->new(); + +Finds a FloatingMatrix from Koha::Caches or instantiates a new one. + +=cut + +sub new { + my ($class) = @_; + + my $cache = Koha::Caches->get_instance(); + my $fm = $cache->get_from_cache('floatingMatrix'); + unless ($fm) { + $fm = {}; + bless $fm, $class; + $fm->_getFloatingMatrix(); + $fm->_createModificationBuffer(); + $cache->set_in_cache('floatingMatrix', $fm, {expiry => $cacheExpiryTime}); + } + return $fm if blessed($fm) && $fm->isa('Koha::FloatingMatrix'); + return undef; +} + +sub _getFloatingMatrix { + my ($fm) = @_; + + my $schema = Koha::Database->new()->schema(); + my @rules = $schema->resultset('FloatingMatrix')->search({}); + + my %map; + $fm->_setBranchRules( \%map ); + foreach my $rule (@rules) { + my $branchRule = Koha::FloatingMatrix::BranchRule->newFromDBIx($rule); + $fm->_linkBranchRule($branchRule); + } +} + +=head GetFloatingTypes +Static Method + + my $floatingTypes = Koha::FloatingMatrix::GetFloatingTypes(); + +@RETURNS Reference to ARRAY, of koha.floating_matrix.floating enumerations. + These are all the available ways Items can float in Koha. +=cut + +sub GetFloatingTypes { + my $schema = Koha::Database->new()->schema(); + my $source = $schema->source('FloatingMatrix'); + my $info = $source->column_info('floating'); + my $floatingTypeEnumerations = $info->{extra}->{list}; + return $floatingTypeEnumerations; +} + +=head getFloatingTypes + $fm->getFloatingTypes(); +See. GetFloatingTypes() +=cut +sub getFloatingTypes { + my ($fm) = @_; + + return $fm->{floatingTypes} if $fm->{floatingTypes}; + + $fm->{floatingTypes} = Koha::FloatingMatrix::GetFloatingTypes(); + return $fm->{floatingTypes}; +} + +=head upsertBranchRule + + my ($branchRule, $error) = $fm->upsertBranchRule( $params ); + if ($error) { + #I must have given some bad object properties + } + else { + $fm->store(); + } + +Adds or modifies an existing overduerule. Take note that this is only a local modification +for this instance of OverdueRulesMap-object. +If changes need to persists, call the $orm->store() method to save changes to DB. + +@PARAM1, HASH, See Koha::Overdues::OverdueRule->new() for parameter descriptions. +@RETURN, Koha::Overdues::OverdueRule-object if success and + String errorCode, if something bad hapened. + +@THROWS Koha::Exceptions::BadParameter from Koha::FloatingMatrix::BranchRule->new(). +=cut + +sub upsertBranchRule { + my ($fm, $params) = @_; + + my $branchRule = Koha::FloatingMatrix::BranchRule->new($params); #While creating this object we sanitate the parameters. + + my $existingBranchRule = $fm->_findBranchRule($branchRule); + my $operation; + if ($existingBranchRule) { #We are modifying an existing rule + #Should we somehow tell that we are updating an existing rule? + $existingBranchRule->replace($branchRule); #Replace with the new sanitated values, we preserve the internal data structure position. + #Replacing might be less costly than recursively unlinking a large internal mapping. + $branchRule = $existingBranchRule; + $operation = 'MOD'; + } + else { #Just adding more rules + $fm->_linkBranchRule($branchRule); + $operation = 'ADD'; + } + + $fm->_pushToModificationBuffer([$branchRule, $operation]); + return $branchRule; +} + +=head getBranchRule + + my $branchRule = $fm->getBranchRule( $fromBranch, $toBranch ); + +@PARAM1 String, koha.floating_matrix.from_branch, must give @PARAM2 as well +@PARAM2 String, koha.floating_matrix.to_branch, must give @PARAM1 as well +@RETURNS Koha::FloatingMatrix::BranchRule-object matching the params or undef. +=cut + +sub getBranchRule { + my ($fm, $fromBranch, $toBranch) = @_; + + my $branchRule = $fm->_findBranchRule(undef, $fromBranch, $toBranch); + + return $branchRule; +} + +=head getBranchRules + + my $branchRules = $fm->getBranchRules(); + +@RETURNS Reference to a HASH of Koha::FloatingMatrix::BranchRule-objects + Hash keys are -, eg. FFL-CPL +=cut + +sub getBranchRules { + my ($fm) = @_; + return $fm->{branchRules}; +} + +=head _setBranchRules +Needs to be called only from the _getFloatingMatrix() to bind the branchRules-map to this object. +@PARAM1, Reference to HASH. +=cut + +sub _setBranchRules { + my ($fm, $hash) = @_; + $fm->{branchRules} = $hash; +} + +sub checkFloating { + my ($fm, $item, $checkinBranch, $transferTargetBranch) = @_; + unless ($transferTargetBranch) { #If no transfer branch is given, then we try our best to figure it out. + my $branchItemRule = C4::Circulation::GetBranchItemRule($item->{'homebranch'}, $item->{'itype'}); + my $returnBranchRule = $branchItemRule->{'returnbranch'} || "homebranch"; + # get the proper branch to which to return the item + $transferTargetBranch = $item->{$returnBranchRule} || C4::Context->userenv->{'branch'}; + } + + #If the check-in branch and transfer branch are the same, then no point in transferring. + if ($checkinBranch eq $transferTargetBranch) { + return undef; + } + + my $branchRule = $fm->getBranchRule($checkinBranch, $transferTargetBranch); + if ($branchRule) { + my $floating = $branchRule->getFloating(); + if ($floating eq 'ALWAYS') { + return 'ALWAYS'; + } + elsif ($floating eq 'POSSIBLE') { + return 'POSSIBLE'; + } + elsif ($floating eq 'CONDITIONAL') { + if(_CheckConditionalFloat($item, $branchRule)) { + return 'ALWAYS'; + } + } + else { + warn "FloatingMatrix->checkFloating():> Bad floating type for route from '$checkinBranch' to '$transferTargetBranch'. Not floating.\n"; + } + } + return undef; +} + +=head _CheckConditionalFloat +Static method + +@PARAM1, HASH of koha.items-row +@PARAM2, Koha::FloatingMatrix::BranchRule-object +@RETURNS 1 if floats, undef if not. +=cut + +sub _CheckConditionalFloat { + my ($item, $branchRule) = @_; + + my $conditionRules = $branchRule->getConditionRules(); + + my $evalCondition = ''; + if (my @conds = $conditionRules =~ /(\w+)\s+(ne|eq|gt|lt|<|>|==|!=)\s+(\w+)\s*(and|or|xor|&&|\|\|)?/ig) { + #Iterate the condition quads, with the fourth index being the logical join operator. + for (my $i=0 ; $i$column",$operator,"'$value'",$join,''); + } + } + else { + warn "Koha::FloatingMatrix::_CheckConditionalFloat():> Bad condition rules '$conditionRules' couldn't be parsed\n"; + return undef; + } + my $ok = eval("return 1 if($evalCondition);"); + if ($@) { + warn "Koha::FloatingMatrix::_CheckConditionalFloat():> Something bad hapened when trying to evaluate the dynamic conditional:\n$@\n"; + return undef; + } + return $ok; +} + +=head CheckFloating +Static Subroutine + +A convenience Subroutine to checkFloating without intantiating a new Koha::FloatingMatrix-object +=cut + +sub CheckFloating { + my ($item, $checkinBranch, $transferTargetBranch) = @_; + my $fm = Koha::FloatingMatrix->new(); + return $fm->checkFloating($item, $checkinBranch, $transferTargetBranch); +} + +=head _findBranchRule + + my $branchRule = $fm->_findBranchRule( undef, $fromBranch, $toBranch ); + my $existingBranchRule = $fm->_findBranchRule( $branchRule ); + +Finds a branch rule from the internal floating matrix map. + +This abstracts the retrieval of branch rules, so we can later change the internal mapping. +@PARAM1, Koha::FloatingMatrix::BranchRule-object, to see if a BranchRule with same targeting rules is present. + or undef, if you are only interested in retrieving. +@PARAM2-3, MANDATORY if no @PARAM1 given, Targeting rules to find a BranchRule. +@RETURNS Koha::FloatingMatrix::BranchRule-object, of the object occupying the given position. + or undef if nothing is found. +=cut + +sub _findBranchRule { + my ($fm, $branchRule, $fromBranch, $toBranch) = @_; + if (blessed($branchRule) && $branchRule->isa('Koha::FloatingMatrix::BranchRule')) { + $fromBranch = $branchRule->getFromBranch(); + $toBranch = $branchRule->getToBranch(); + } + + my $existingBranchRule = $fm->getBranchRules()->{ $fromBranch }->{ $toBranch }; + return $existingBranchRule; +} + +=head _linkBranchRule + + my $existingBranchRule = $fm->_linkBranchRule( $branchRule ); + +Links the new branchrule to the internal floating matrix map, overwriting a possible existing +reference to a BranchRule, and losing that into the binary limbo. + +This abstracts the retrieval of floating matrix routes, so we can later change the internal mapping. +@PARAM1, Koha::FloatingMatrix::BranchRule-object, to link to the internal mapping +=cut + +sub _linkBranchRule { + my ($fm, $branchRule) = @_; + + $fm->{branchRules}->{ $branchRule->getFromBranch() }->{ $branchRule->getToBranch() } = $branchRule; +} + +=head _unlinkBranchRule + + my $existingBranchRule = $fm->_unlinkBranchRule( $branchRule ); + +Unlinks the branchRule from the internal floating matrix map and releasing it into the binary limbo. + +This abstracts the internal mapping of branch rules, so we can later change it. +@PARAM1, Koha::FloatingMatrix::BranchRule-object + +=cut + +sub _unlinkBranchRule { + my ($fm, $branchRule) = @_; + + #Delete the BranchRule + my $branchRules = $fm->getBranchRules(); + my $fromBranch = $branchRule->getFromBranch(); + my $toBranch = $branchRule->getToBranch(); + + eval{ delete( $branchRules->{ $fromBranch }->{ $toBranch } ); }; + if ($@) { + carp "Unlinking BranchRule failed because of '".$@."'. Something wrong with this BranchRule\n".$branchRule->toString()."\n"; + return $@; + } + + unless (scalar(%{$branchRules->{ $fromBranch }})) { + #Delete the branchLevel + delete( $branchRules->{ $fromBranch } ); + } +} + +=head deleteBranchRule + + $fm->deleteBranchRule($branchRule); + $fm->deleteBranchRule($fromBranch, $toBranch); + +Deletes the BranchRule from the internal mapping, but not from the DB. +Call $fm->store() to persist the removal. + +The given $branchRule must be the same object gained using $fm->getBranchRule(), +or you are misusing the FloatingMatrix-object and bad things might happen. + +@PARAM1, Koha::FloatingMatrix::BranchRule-object to delete from DB. +or +@PARAM1 {String, koha.floating_matrix.from_branch} +@PARAM1 {String, koha.floating_matrix.to_branch} + +@THROWS Koha::Exceptions::BadParameter if no branchRule, with the given parameters, is found, + or the given BranchRule doesn't match the one in the internal mapping. +=cut + +sub deleteBranchRule { + my ($fm, $fromBranch, $toBranch) = @_; + my ($branchRule, $branchRuleLocal); + #Process given parameters, see if we use parameter group1 (BranchRule-object) or group2 (branchcodes)? + if (blessed $fromBranch && $fromBranch->isa('Koha::FloatingMatrix::BranchRule')) { + $branchRule = $fromBranch; + $fromBranch = $branchRule->getFromBranch(); + $toBranch = $branchRule->getToBranch(); + } + + $branchRuleLocal = $fm->getBranchRule($fromBranch, $toBranch); + Koha::Exceptions::BadParameter->throw(error => "Koha::FloatingMatrix->deleteBranchRule($fromBranch, $toBranch):> No BranchRule exists for the given fromBranch '$fromBranch' and toBranch '$toBranch'") + unless $branchRuleLocal; + + $fm->_unlinkBranchRule( $branchRuleLocal ); + $fm->_pushToModificationBuffer([$branchRuleLocal, 'DEL']); +} + +=head $fm->deleteAllFloatingMatrixRules() + +Deletes all Floating matrix rules in the DB, shows no pity. +Invalidates $fm in the Koha::Caches +=cut + +sub deleteAllFloatingMatrixRules { + my ($fm) = @_; + my $schema = Koha::Database->new()->schema(); + $schema->resultset('FloatingMatrix')->search({})->delete_all; + $fm = undef; + + my $cache = Koha::Caches->get_instance(); + $cache->clear_from_cache('floatingMatrix'); +} + +=head store + + $fm->store(); + +Saves all pending transactions to DB, by calling the Koha::FloatingMatrix::BranchRule->store() || delete(); + +=cut + +sub store { + my $fm = shift; + my $schema = Koha::Database->new()->schema(); + + my $pendingModifications = $fm->_consumeModificationBuffer(); + foreach my $modRequest (@$pendingModifications) { + my $branchRule = $modRequest->[0]; + my $operation = $modRequest->[1]; + if ($operation eq 'MOD' || $operation eq 'ADD') { + $branchRule->store(); + } + elsif ($operation eq 'DEL') { + $branchRule->delete(); + } + else { + carp "Unsupported database access operation '$operation'!"; + } + } + + my $cache = Koha::Caches->get_instance(); + $cache->set_in_cache('floatingMatrix', $fm, {expiry => $cacheExpiryTime}); +} + +=head _pushToModificationBuffer + + $fm->_pushToModificationBuffer([$branchRule, $operation]); + +To be able to more effectively service DB write operations, especially when using +FloatingMatrix with an REST(ful?) API giving lots of write operations, it is useful +to be able to know which BranchRules need changing and which do not. +Thus we don't need to either +DELETE all BranchRules from DB and re-add them (what if the rewrite request dies?) + or +check each rule for possible changes. + +The modification buffer tells what information needs changing. + +To write the changes to DB, use $fm->store(). + +@PARAM1, Tuple (Two-index ARRAY reference). [0] = $branchRule (see. upsertBranchRule()) + [1] = The operation, either 'ADD', 'MOD' or 'DEL' +@RETURN, always nothing. + +=cut + +sub _pushToModificationBuffer { + my ($fm, $tuple) = @_; + push @{$fm->{modBuffer}}, $tuple; + return undef; +} +=head _consumeModificationBuffer +Detaches the modification buffer from the FloatingMatrix (parent) and returns it. +Fm now has an empty modification buffer ready for new modifications. +=cut + +sub _consumeModificationBuffer { + my ($fm) = @_; + my $modBuffer = $fm->{modBuffer}; + $fm->{modBuffer} = []; + return $modBuffer; +} +sub _createModificationBuffer { + my ($fm) = @_; + $fm->{modBuffer} = []; + return undef; +} + +1; #Satisfy the compiler \ No newline at end of file diff --git a/Koha/FloatingMatrix/BranchRule.pm b/Koha/FloatingMatrix/BranchRule.pm new file mode 100644 index 0000000000..c597739b5a --- /dev/null +++ b/Koha/FloatingMatrix/BranchRule.pm @@ -0,0 +1,342 @@ +package Koha::FloatingMatrix::BranchRule; + +# Copyright 2015 Vaara-kirjastot +# +# 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 2 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 Rule + +Koha::FloatingMatrix::BranchRule - Object representing floating rules for one branch + +=head1 DESCRIPTION + + +=cut + +use Modern::Perl; +use Carp qw(carp croak confess longmess); +use Scalar::Util 'blessed'; +use Data::Dumper; + +use C4::Context qw(dbh); +use Koha::Cache; +use Koha::Database; + +use Koha::Exceptions::BadParameter; + + +=head new + + my ($branchRule, $error) = Koha::FloatingMatrix::BranchRule->new({ + fromBranch => 'CPL', + toBranch => 'FFL', + floating => ['ALWAYS'|'POSSIBLE'|'CONDITIONAL'], + conditionRules => "items->{itype} eq 'BK' && $items->{permanent_location} eq 'CART'" + }); + +BranchRule defines the floating rule for one transfer route. +Eg. If you check-in an Item to CFL and based on your library policies tha Item is transferred to IPT, +We check if there is a floating rule for that fromBranch-toBranch -combination and apply that if applicable. + +@PARAM1, HASH +@RETURN, Koha::FloatingMatrix::BranchRule-object, +@THROWS Koha::Exceptions::BadParameter if given parameters don't validate properly. +=cut + +sub new { + my ($class, $params) = @_; + + ValidateParams($params); + + bless $params, $class; + return $params; +} + +=head newFromDBIx + + Koha::FloatingMatrix::BranchRule->newFromDBIx( $Koha::Schema::Result::FloatingMatrix ); + +Creates a BranchRule-object from DBIx. +See new() for more info. +=cut + +sub newFromDBIx { + my ($class, $dbix) = @_; + + my $params = { + id => $dbix->id(), + fromBranch => $dbix->get_column('from_branch'), + toBranch => $dbix->get_column('to_branch'), + floating => $dbix->get_column('floating'), + conditionRules => $dbix->get_column('condition_rules'), + }; + return $class->new($params); +} + +=head ValidateParams, Static subroutine + + my $error = Koha::Overdues::OverdueRule::ValidateParams($params); + +@PARAM1, HASH, see new() for valid values. +@THROWS Koha::Exceptions::BadParameter if given parameters don't validate properly. +=cut + +my $maximumConditionRulesDatabaseLength = 100; +sub ValidateParams { + my ($params) = @_; + my $errorMsg; + + if (not($params->{fromBranch}) || length $params->{fromBranch} < 1) { + $errorMsg = "No 'fromBranch'"; + } + elsif (not($params->{toBranch}) || length $params->{toBranch} < 1) { + $errorMsg = "No 'toBranch'"; + } + elsif (not($params->{floating}) || length $params->{floating} < 1) { + $errorMsg = "No 'floating'"; + } + elsif ($params->{floating} && ($params->{floating} ne 'CONDITIONAL' && + $params->{floating} ne 'ALWAYS' && + $params->{floating} ne 'POSSIBLE') + ) { + $errorMsg = "Bad enum '".$params->{floating}."' for 'floating'"; + } + elsif (not($params->{conditionRules}) && $params->{floating} eq 'CONDITIONAL') { + $errorMsg = "No 'conditionRules' when floating = 'CONDITIONAL'"; + } + elsif ($params->{conditionRules} && $params->{conditionRules} =~ /[};{]/gsmi) { + $errorMsg = "Not allowed 'conditionRules' characters '};{' present"; + } + elsif ($params->{conditionRules} && length($params->{conditionRules}) > $maximumConditionRulesDatabaseLength) { + $errorMsg = "'conditionRules' text is too long. Maximum length is '$maximumConditionRulesDatabaseLength' characters"; + } + elsif ($params->{conditionRules}) { + ParseConditionRules(undef, $params->{conditionRules}); + } + + if ($errorMsg) { + my $fb = $params->{fromBranch} || ''; + my $tb = $params->{toBranch} || ''; + my $id = $params->{id} || ''; + Koha::Exceptions::BadParameter->throw(error => "Koha::FloatingMatrix::BranchRule::ValidateParams():> $errorMsg. For branch rule id '$id', fromBranch '$fb', toBranch '$tb'."); + } + #Now that we have sanitated the input, we can rest assured that bad input won't crash this Object :) +} + +=head parseConditionRules, Static method + + my $evalCondition = ParseConditionRules($item, $conditionRules); + my $evalCondition = ParseConditionRules(undef, $conditionRules); + +Parses the given Perl boolean expression into an eval()-able expression. +If Item is given, uses the Item's columns to create a executable expression to check for +conditional floating for this specific Item. + +@PARAM1 {Reference to HASH of koha.items-row} Item to check for conditional floating. +@PARAM2 {String koha.floating_matrix.condition_rules} The boolean expression to turn + into a Perl code to check the floating condition. +@THROWS Koha::Exceptions::BadParameter, if the conditionRules couldn't be parsed. +=cut +sub ParseConditionRules { + my ($item, $conditionRulesString) = @_; + my $evalCondition = ''; + if (my @conds = $conditionRulesString =~ /(\w+)\s+(ne|eq|gt|lt|<|>|==|!=)\s+(\w+)\s*(and|or|xor|&&|\|\|)?/ig) { + + #If we haven't got no Item, simply stop here to aknowledge that the given condition logic is valid (atleast parseable) + return undef unless $item; + + #If we have an Item, then prepare and return an eval-expression to test if the Item should float. + #Iterate the condition quads, with the fourth index being the logical join operator. + for (my $i=0 ; $i{'$column'}",$operator,"'$value'",$join,''); + } + + return $evalCondition; + } + else { + Koha::Exceptions::BadParameter->throw(error => + "Koha::FloatingMatrix::parseConditionRules():> Bad condition rules '$conditionRulesString' couldn't be parsed\n". + "See 'Help' for more info"); + } +} + +=head replace + + my $fmBranchRule->replace( $replacementBranchRule ); + +Replaces the calling branch rule's keys with the given parameters'. +=cut + +sub replace { + my ($branchRule, $replacementBranchRule) = @_; + + foreach my $key (keys %$replacementBranchRule) { + $branchRule->{$key} = $replacementBranchRule->{$key}; + } +} + +=head clone + $fmBranchCode->clone(); +Returns a duplicate of self +=cut +sub clone { + my ($branchRule) = @_; + + my %newBranchRuleParams; + foreach my $key (keys %$branchRule) { + $newBranchRuleParams{$key} = $branchRule->{$key}; + } + return Koha::FloatingMatrix::BranchRule->new(\%newBranchRuleParams); +} + +=head store +Saves the BranchRule into the floating_matrix-table +@THROWS Koha::Exceptions::BadParameter if id given but no object exists with that id. +=cut +sub store { + my $branchRule = shift; + my $schema = Koha::Database->new()->schema(); + + my $params = $branchRule->_buildBranchRuleColumns(); + my $id = $branchRule->getId(); + my $oldBranchRule; + if ($id) { + $oldBranchRule = $schema->resultset( 'FloatingMatrix' )->find( $id ); + } + if ($id && not($oldBranchRule)) { + Koha::Exceptions::BadParameter->throw(error => "Koha::FloatingMatrix::BranchRule->store():> floating_matrix.id given, but no matching row exist in DB"); + } + + if ($oldBranchRule) { + $oldBranchRule->update( $params ); + } + else { + my $newBranchRule = $schema->resultset( 'FloatingMatrix' )->create( $params ); + $branchRule->setId( $newBranchRule->id() ); + } +} + +=head _buildBranchRuleColumns +Transforms the BranchRule into a DBIx $parameters HASH which can be UPDATED to DB. +DBIx is crazy about excess parameters with no mapped DB column, so we cannot just pass the +BranchRule-object to the DBIx. +=cut +sub _buildBranchRuleColumns { + my ($branchRule) = @_; + + my $columns = {}; + $columns->{"from_branch"} = $branchRule->getFromBranch(); + $columns->{"to_branch"} = $branchRule->getToBranch(); + $columns->{"floating"} = $branchRule->getFloating(); + $columns->{"condition_rules"} = $branchRule->getConditionRules(); + + return $columns; +} + +sub delete { + my ($branchRule) = @_; + my $schema = Koha::Database->new()->schema(); + $schema->resultset('FloatingMatrix')->find($branchRule->getId())->delete(); +} + +sub setId { + my ($self, $val) = @_; + if ($val) { + $self->{id} = $val; + } + else { + delete $self->{id}; + } +} +sub getId { + my ($self) = @_; + return $self->{id}; +} +sub setFromBranch { + my ($self, $fromBranch) = @_; + $self->{fromBranch} = $fromBranch; +} +sub getFromBranch { + my ($self) = @_; + return $self->{fromBranch}; +} +sub setToBranch { + my ($self, $toBranch) = @_; + $self->{toBranch} = $toBranch; +} +sub getToBranch { + my ($self) = @_; + return $self->{toBranch}; +} +sub setFloating { + my ($self, $val) = @_; + $self->{floating} = $val; +} +sub getFloating { + my ($self) = @_; + return $self->{floating}; +} +=head setConditionRules +See parseConditionRules() +@THROWS Koha::Exceptions::BadParameter, if the conditionRules couldn't be parsed. +=cut +sub setConditionRules { + my ($self, $val) = @_; + #Validate the conditinal rules. + ParseConditionRules(undef, $val); + $self->{conditionRules} = $val; +} +sub getConditionRules { + my ($self) = @_; + return $self->{conditionRules}; +} + +=head toString + + my $stringRepresentationOfThisObject = $branchRule->toString(); + print $stringRepresentationOfThisObject."\n"; + +=cut + +sub toString { + my ($self) = @_; + + return Data::Dumper::Dump($self); +} +=head TO_JSON + + my $json = JSON::XS->new->utf8->convert_blessed->encode( $branchRule ); + or + my $json = $branchRule->TO_JSON(); + +Used to serialize this object as JSON. +=cut +sub TO_JSON { + my ($branchRule) = @_; + + my $json = {}; + while (my ($key, $val) = each(%$branchRule)) { + $json->{$key} = $val; + } + return $json; +} + +1; #Satisfy the compiler \ No newline at end of file diff --git a/Koha/Schema/Result/Branch.pm b/Koha/Schema/Result/Branch.pm index 6680195079..21057a582b 100644 --- a/Koha/Schema/Result/Branch.pm +++ b/Koha/Schema/Result/Branch.pm @@ -526,6 +526,36 @@ __PACKAGE__->has_many( { cascade_copy => 0, cascade_delete => 0 }, ); +=head2 floating_matrixes_from_branch + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "floating_matrixes_from_branch", + "Koha::Schema::Result::FloatingMatrix", + { "foreign.from_branch" => "self.branchcode" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 floating_matrixes_to_branch + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "floating_matrixes_to_branch", + "Koha::Schema::Result::FloatingMatrix", + { "foreign.to_branch" => "self.branchcode" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + =head2 hold_fill_targets Type: has_many @@ -737,8 +767,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07046 @ 2019-12-26 10:51:35 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vxx2lpU/W6cC5muhVAO1jw +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-03-10 09:28:20 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5BLRaKzrxKXDbnPoGpdZPg __PACKAGE__->add_columns( '+pickup_location' => { is_boolean => 1 } diff --git a/Koha/Schema/Result/FloatingMatrix.pm b/Koha/Schema/Result/FloatingMatrix.pm new file mode 100644 index 0000000000..26d273aab4 --- /dev/null +++ b/Koha/Schema/Result/FloatingMatrix.pm @@ -0,0 +1,145 @@ +use utf8; +package Koha::Schema::Result::FloatingMatrix; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::FloatingMatrix + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("floating_matrix"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 from_branch + + data_type: 'varchar' + is_foreign_key: 1 + is_nullable: 0 + size: 10 + +=head2 to_branch + + data_type: 'varchar' + is_foreign_key: 1 + is_nullable: 0 + size: 10 + +=head2 floating + + data_type: 'enum' + default_value: 'ALWAYS' + extra: {list => ["ALWAYS","POSSIBLE","CONDITIONAL"]} + is_nullable: 0 + +=head2 condition_rules + + data_type: 'varchar' + is_nullable: 1 + size: 100 + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "from_branch", + { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 }, + "to_branch", + { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 }, + "floating", + { + data_type => "enum", + default_value => "ALWAYS", + extra => { list => ["ALWAYS", "POSSIBLE", "CONDITIONAL"] }, + is_nullable => 0, + }, + "condition_rules", + { data_type => "varchar", is_nullable => 1, size => 100 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("floating_matrix_uniq_branches", ["from_branch", "to_branch"]); + +=head1 RELATIONS + +=head2 from_branch + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "from_branch", + "Koha::Schema::Result::Branch", + { branchcode => "from_branch" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + +=head2 to_branch + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "to_branch", + "Koha::Schema::Result::Branch", + { branchcode => "to_branch" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-03-10 09:28:20 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jOVU1Wag0o96NkjrnPjsvg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/admin/floating-matrix-api.pl b/admin/floating-matrix-api.pl new file mode 100755 index 0000000000..2c62c10c81 --- /dev/null +++ b/admin/floating-matrix-api.pl @@ -0,0 +1,98 @@ +#!/usr/bin/perl +# Copyright 2015 Vaara-kirjastot +# +# 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 2 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 CGI qw ( -utf8 ); +use Try::Tiny; +use Scalar::Util qw(blessed); +use JSON::XS; + +use C4::Context; +use C4::Output; +use C4::Auth qw(check_cookie_auth); +use C4::Items; + +use Koha::Items; +use Koha::FloatingMatrix; + +my $input = new CGI; + +#my ( $auth_status, $sessionID ) = +# check_cookie_auth( $input->cookie('CGISESSID'), +# { circulate => 'circulate_remaining_permissions' } ); +my ( $auth_status, $sessionID ) = + check_cookie_auth( $input->cookie('CGISESSID'), + { + parameters => 1, + } ); + + +binmode STDOUT, ":encoding(UTF-8)"; + +my $fm = Koha::FloatingMatrix->new(); + +my $data = $input->Vars(); +if ($data) { + try { + ##If we are getting a DELETE-request, we DELETE (CGI doesn't know what DELETE is :((( + if ($data->{delete}) { + $fm->deleteBranchRule($data->{fromBranch}, $data->{toBranch}); + $fm->store(); + } + elsif ($data->{test}) { + my $item = Koha::Items->find({ barcode => $data->{barcode} }); + if ($data->{barcode} && $item) { + $data->{testResult} = $fm->checkFloating($item, $data->{fromBranch}, $data->{toBranch}); + } + else { + Koha::Exceptions::BadParameter->throw(error => "No Item found using barcode '".$data->{barcode}."'"); + } + } + ##If we are getting a POST-request, we UPSERT + else { + $fm->upsertBranchRule($data); + $fm->store(); + } + } catch { + if (blessed $_ && $_->isa('Koha::Exceptions::BadParameter')) { + respondBadParameterException($_); + } + else { + die $_; + } + }; + + print $input->header( -type => 'text/json', + -charset => 'UTF-8', + -status => "200 OK"); + print JSON::XS->new->utf8->convert_blessed->encode($data); +} +else { + print $input->header( -type => 'text/plain', + -charset => 'UTF-8', + -status => "405 Method Not Allowed"); +} + +sub respondBadParameterException { + my ($e) = @_; + print $input->header( -type => 'text/json', + -charset => 'UTF-8', + -status => "400 Bad Request"); + print JSON::XS->new->utf8->encode({error => $e->as_string()}); + exit 1; +} \ No newline at end of file diff --git a/admin/floating-matrix.pl b/admin/floating-matrix.pl new file mode 100755 index 0000000000..d17d3175cf --- /dev/null +++ b/admin/floating-matrix.pl @@ -0,0 +1,138 @@ +#!/usr/bin/perl +# Copyright 2015 Vaara-kirjastot +# +# 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 2 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 CGI qw ( -utf8 ); +use C4::Context; +use C4::Output; +use C4::Auth; + +use Koha::FloatingMatrix; + +my $input = new CGI; + +my ($template, $loggedinuser, $cookie) + = get_template_and_user({template_name => "admin/floating-matrix.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => {parameters => 1}, + debug => 1, + }); + +my $operation = 'showMatrix'; #Default op is show. + +my $fm = Koha::FloatingMatrix->new(); +my $branches = Koha::Libraries->search(); + +$template->param( + fm => $fm, + branches => $branches, +); + + +output_html_with_http_headers $input, $cookie, $template->output; + +exit 0; + +=head +my $update = $input->param('op') eq 'set-cost-matrix'; + +my ($cost_matrix, $have_matrix); +unless ($update) { + $cost_matrix = TransportCostMatrix(); + $have_matrix = keys %$cost_matrix if $cost_matrix; +} + +my $branches = GetBranches(); +my @branchloop = map { code => $_, + name => $branches->{$_}->{'branchname'} }, + sort { $branches->{$a}->{branchname} cmp $branches->{$b}->{branchname} } + keys %$branches; +my (@branchfromloop, @cost, @errors); +foreach my $branchfrom ( @branchloop ) { + my $fromcode = $branchfrom->{code}; + + my %from_row = ( code => $fromcode, name => $branchfrom->{name} ); + foreach my $branchto ( @branchloop ) { + my $tocode = $branchto->{code}; + + my %from_to_input_def = ( code => $tocode, name => $branchto->{name} ); + push @{ $from_row{branchtoloop} }, \%from_to_input_def; + + if ($fromcode eq $tocode) { + $from_to_input_def{skip} = 1; + next; + } + + (my $from_to = "${fromcode}_${tocode}") =~ s/\W//go; + $from_to_input_def{id} = $from_to; + my $input_name = "cost_$from_to"; + my $disable_name = "disable_$from_to"; + + if ($update) { + my $value = $from_to_input_def{value} = $input->param($input_name); + if ( $input->param($disable_name) ) { + $from_to_input_def{disabled} = 1; + } + else { + push @errors, "$from_row{name} -> $from_to_input_def{name}" + unless $value =~ /\d/o && $value >= 0.0; + } + } + else { + if ($have_matrix) { + if ( my $cell = $cost_matrix->{$tocode}{$fromcode} ) { + $from_to_input_def{value} = $cell->{cost}; + $from_to_input_def{disabled} = 1 if $cell->{disable_transfer}; + } else { + # matrix has been previously initialized, but a branch referenced here was created afterward. + $from_to_input_def{disabled} = 1; + } + } else { + # First time initializing the matrix + $from_to_input_def{disabled} = 1; + } + } + } + +# die Dumper(\%from_row); + push @branchfromloop, \%from_row; +} + +if ($update && !@errors) { + my @update_recs = map { + my $from = $_->{code}; + map { frombranch => $from, tobranch => $_->{code}, cost => $_->{value}, disable_transfer => $_->{disabled} || 0 }, + grep { $_->{code} ne $from } + @{ $_->{branchtoloop} }; + } @branchfromloop; + + UpdateTransportCostMatrix(\@update_recs); +} + +$template->param( + branchloop => \@branchloop, + branchfromloop => \@branchfromloop, + errors => \@errors, +); +output_html_with_http_headers $input, $cookie, $template->output; + +exit 0; + +=cut \ No newline at end of file diff --git a/installer/data/mysql/atomicupdate/Bug-9525_group_floating_rules.perl b/installer/data/mysql/atomicupdate/Bug-9525_group_floating_rules.perl new file mode 100644 index 0000000000..d8241472f1 --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug-9525_group_floating_rules.perl @@ -0,0 +1,26 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + + #Check if the table has already been CREATEd and possibly CREATE it + my $dbh = C4::Context->dbh(); + my $sth = $dbh->table_info( '', '', 'floating_matrix', 'TABLE' ); + my $table = $sth->fetchrow_hashref(); + unless ($table) { + $dbh->do(" + CREATE TABLE floating_matrix ( -- Controls should we automatically transfer Items checked in to one branch to the Item's configured normal destination + id int(11) NOT NULL AUTO_INCREMENT, -- unique id + from_branch varchar(10) NOT NULL, -- branch where the Item has been checked in + to_branch varchar(10) NOT NULL, -- where the Item would normally be transferred to + floating enum('ALWAYS','POSSIBLE','CONDITIONAL') NOT NULL DEFAULT 'ALWAYS', -- type of floating; ALWAYS just skips any transports, POSSIBLE prompts if a transport is needed, CONDITIONAL is like ALWAYS if condition is met + condition_rules varchar(100), -- if floating = CONDITIONAL, then the special condition to trigger floating. + CHECK ( from_branch <> to_branch ), -- a dud check, mysql does not support that + PRIMARY KEY (`id`), + UNIQUE KEY `floating_matrix_uniq_branches` (`from_branch`,`to_branch`), + CONSTRAINT floating_matrix_ibfk_1 FOREIGN KEY (from_branch) REFERENCES branches (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT floating_matrix_ibfk_2 FOREIGN KEY (to_branch) REFERENCES branches (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + SetVersion( $DBversion ); + print "Upgrade to $DBversion done (Bug 9525 - group floating rules)\n"; + } +} diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 8ec1791c66..e9cbe9975d 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -287,6 +287,24 @@ CREATE TABLE IF NOT EXISTS branches_overdrive ( CONSTRAINT `branches_overdrive_ibfk_1` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- +-- Table structure for table `floating_matrix` +-- + +DROP TABLE IF EXISTS floating_matrix; +CREATE TABLE floating_matrix ( -- Controls should we automatically transfer Items checked in to one branch to the Item's configured normal destination + id int(11) NOT NULL AUTO_INCREMENT, -- unique id + from_branch varchar(10) NOT NULL, -- branch where the Item has been checked in + to_branch varchar(10) NOT NULL, -- where the Item would normally be transferred to + floating enum('ALWAYS','POSSIBLE','CONDITIONAL') NOT NULL DEFAULT 'ALWAYS', -- type of floating; ALWAYS just skips any transports, POSSIBLE prompts if a transport is needed, CONDITIONAL is like ALWAYS if condition is met + condition_rules varchar(100), -- if floating = CONDITIONAL, then the special condition to trigger floating. + CHECK ( from_branch <> to_branch ), -- a dud check, mysql does not support that + PRIMARY KEY (`id`), + UNIQUE KEY `floating_matrix_uniq_branches` (`from_branch`,`to_branch`), + CONSTRAINT floating_matrix_ibfk_1 FOREIGN KEY (from_branch) REFERENCES branches (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT floating_matrix_ibfk_2 FOREIGN KEY (to_branch) REFERENCES branches (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- -- Table structure for table `browser` -- diff --git a/koha-tmpl/intranet-tmpl/prog/css/floating-matrix-loader.css b/koha-tmpl/intranet-tmpl/prog/css/floating-matrix-loader.css new file mode 100644 index 0000000000..511c6eae9d --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/css/floating-matrix-loader.css @@ -0,0 +1,78 @@ +.cssload-loader { + position: relative; + left: calc(50% - 8px); + width: 16px; + height: 16px; + border-radius: 50%; + -moz-border-radius: 50%; + perspective: 200px; + margin-top: 3px; + visibility: hidden; +} + +.cssload-inner { + position: absolute; + width: 100%; + height: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + border-radius: 50%; + -moz-border-radius: 50%; +} + +.cssload-inner.cssload-one { + left: 0%; + top: 0%; + animation: cssload-rotate-one 1.3s linear infinite; + -moz-animation: cssload-rotate-one 1.3s linear infinite; + border-bottom: 2px solid rgb(0,0,0); +} + +.cssload-inner.cssload-two { + right: 0%; + top: 0%; + animation: cssload-rotate-two 1.3s linear infinite; + -moz-animation: cssload-rotate-two 1.3s linear infinite; + border-right: 2px solid rgb(0,0,0); +} + +.cssload-inner.cssload-three { + right: 0%; + bottom: 0%; + animation: cssload-rotate-three 1.3s linear infinite; + -moz-animation: cssload-rotate-three 1.3s linear infinite; + border-top: 2px solid rgba(0,0,0,0.99); +} + + + + + + + +@keyframes cssload-rotate-one { + 0% { + transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg); + } + 100% { + transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg); + } +} + +@keyframes cssload-rotate-two { + 0% { + transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg); + } + 100% { + transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg); + } +} + +@keyframes cssload-rotate-three { + 0% { + transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); + } + 100% { + transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); + } +} diff --git a/koha-tmpl/intranet-tmpl/prog/css/floating-matrix.css b/koha-tmpl/intranet-tmpl/prog/css/floating-matrix.css new file mode 100644 index 0000000000..14da50bca3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/css/floating-matrix.css @@ -0,0 +1,24 @@ +.disabled-transfer { + background-color: #FF8888; +} + +#testResulDisplay { width: 20px; + height: 22px; + background-color: #B9D8D9; + vertical-align: middle; + display: inline-block; +} + +.branchRule select { width: 100%; } +.branchRule select {padding: 0px; margin: 5px 0px;} + +.branchRule input {padding: 0px; margin: 0px;} +.branchRule input[type="text"] { width: 100%; display: none; } +.branchRule input[type="submit"] {display:none} + +.branchRule.selected {color: #1b93d3; box-shadow:0px 0px 10px;} +.branchRule.selected input[type="submit"] {display:initial;} +.branchRule.selected input[type="text"] {display:initial; border: 0px none;} + +.failedAjax {box-shadow:0px 0px 30px 20px #ff0000} +.TEMPLATE {display: none; visibility: hidden;} \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin/floating-matrix-floatingTypes.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin/floating-matrix-floatingTypes.inc new file mode 100644 index 0000000000..fe208f5923 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin/floating-matrix-floatingTypes.inc @@ -0,0 +1,26 @@ +[%# Summon this block to the template by INCLUDEing it like this in the header: + % INCLUDE 'admin/floating-matrix-floatingType.inc' % + You must have the required parameters in the template toolkit parameter-space. +%] +[%#REQUIRES branchRule (Koha::FloatingMatrix::BranchRule), fm (Koha::FloatingMatrix) %] +[% preselectedFloatingType = branchRule.getFloating() %] + diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index 860d08fd2c..b70acf345b 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -93,6 +93,8 @@ [% IF ( CAN_user_parameters_manage_transfers ) %]
Library transfer limits
Limit the ability to transfer items between libraries based on the library sending, the library receiving, and the item type involved. These rules only go into effect if the preference UseBranchTransferLimits is set to ON.
+
Floating matrix
+
Define floating rules between branches
Transport cost matrix
Define transport costs between branches
[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/floating-matrix.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/floating-matrix.tt new file mode 100644 index 0000000000..7929db0251 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/floating-matrix.tt @@ -0,0 +1,128 @@ +[% USE raw %] +[% USE Asset %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Administration › Floating matrix +[% Asset.css("css/floating-matrix.css") | $raw %] +[% Asset.css("css/floating-matrix-loader.css") | $raw %] +[% INCLUDE 'doc-head-close.inc' %] + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'cat-search.inc' %] + + + +
+ +
+
+
+

+ Defining floating rules between libraries +

+ +
+
+

There are four types of floating:

+
    +
  • Disabled, denoted by red cells
  • +
  • Always, denoted by green cells. This means that Items checked-in to 'From' branch, which would normally be transfered to 'To' branch, stay in the checked-in branch.
  • +
  • Possible, denoted by blue cells. This is like always, except the librarian is prompted if he/she wants to initiate a transfer or not. Default behaviour is not to transfer.
  • +
  • Conditional, denoted by purple cells. This is like always, but only when the given condition matches. +

    + itype eq BK - this will always float all Items with item type 'BK'
    + itype eq BK or itype eq CR - this will always float all Items with item type 'BK' or 'CR'
    + ccode eq FLOAT - this will float all Items with a collection code 'FLOAT'
    + itype ne CR and permanent_location eq CART - this will float all Items not of item type CR and whose permanent location (set when the Item's location is set) is 'CART'
    + These boolean statements are actually evaluable Perl boolean expressions and target the columns in the koha.items-table. See here for available data columns. +

    +
  • +
+
+ +
+ Floating rule tester +
+ + + + + + + +
+
+
+
+
+
+
+ + + + + [% FOREACH branch IN branches %] + + [% END %] + + [% FOREACH fromBranchCode IN branches %] + + + [% FOREACH toBranchCode IN branches; branchRule = fm.getBranchRule(fromBranchCode.branchcode, toBranchCode.branchcode) %] + + [% END %] + + [% END %] +
From \ To[% branch.branchcode %]
[% fromBranchCode.branchcode %] + [% IF toBranchCode.branchcode == fromBranchCode.branchcode %] +   + [% ELSE %] +
+ + [% INCLUDE 'admin/floating-matrix-floatingTypes.inc' %] + + +
+ + +
+
+ [% END %] +
+
+
+
+
+ +
+
+
+ + + + +
+ + [% INCLUDE 'admin/floating-matrix-floatingTypes.inc' %] + + +
+ + +
+
+ + +[% Asset.js("js/floating-matrix.js") | $raw %] +[% INCLUDE 'intranet-bottom.inc' %] \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/js/floating-matrix.js b/koha-tmpl/intranet-tmpl/prog/js/floating-matrix.js new file mode 100644 index 0000000000..508d8e63b8 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/js/floating-matrix.js @@ -0,0 +1,250 @@ +////Create javascript namespace +var FloMax = FloMax || {}; +FloMax.Tester = FloMax.Tester || {}; + +//// DOCUMENT READY INIT SCRIPTS + +FloMax.branchRules = {}; //Stores all the edited branchRules so we can rollback HTML changes and CRUD over AJAX. + +FloMax.branchRuleDisabledColor = "#e83519"; +FloMax.branchRuleAlwaysColor = "#219e0e"; +FloMax.branchRulePossibleColor = "#2626bf"; +FloMax.branchRuleConditionalColor = "#af21bc"; +$(document).ready(function() { + //Color cells based on their floatingType + $(".branchRule").each(function(){ + FloMax.changeBranchRuleColor(this); + }); + + //Bind actions to the FloMax Tester + $("#floatingRuleTester input[type='submit']").bind({ + click: function() { + FloMax.Tester.testFloating(); + } + }); + + //Bind action listeners to the floating matrix + $(".branchRule").bind({ + click: function() { + //If we are clicking a branchCode element, mark it as selected and remove other selections. + if (! $(this).hasClass('selected')) { + $(".branchRule").removeClass('selected'); + $("input[name='conditionRules']").addClass('hidden'); + + $(this).addClass('selected'); + FloMax.changeBranchRuleColor(this); + $(this).find("input[name='conditionRules']").removeClass('hidden'); + } + }, + }); + $(".branchRule input[name='cancel']").bind({ + click: function(event) { + FloMax.replaceBranchRule( $(this).parents(".branchRule") ); + $(this).parents(".branchRule").removeClass('selected'); + $("input[name='conditionRules']").addClass('hidden'); + event.stopPropagation(); + }, + }); + $(".branchRule input[name='submit']").bind({ + click: function(event) { + FloMax.storeBranchRule( $(this).parents(".branchRule") ); + $(this).parents(".branchRule").removeClass('selected'); + $("input[name='conditionRules']").addClass('hidden'); + event.stopPropagation(); + }, + }); + $(".branchRule select").bind({ + change: function() { + //When the floatingType changes, so does the color + var branchRule = $(this).parents(".branchRule"); + FloMax.changeBranchRuleColor(branchRule); + }, + }); +}); +////EOF DOCUMENT READY INIT SCRIPTS + +FloMax.changeBranchRuleColor = function (branchRule) { + var floatingType = $(branchRule).find("select[name='floating']").val(); + var color = "#000000"; + if (floatingType == 'DISABLED') { + color = FloMax.branchRuleDisabledColor; + } + else if (floatingType == 'ALWAYS') { + color = FloMax.branchRuleAlwaysColor; + } + else if (floatingType == 'POSSIBLE') { + color = FloMax.branchRulePossibleColor; + } + else if (floatingType == 'CONDITIONAL') { + color = FloMax.branchRuleConditionalColor; + } + $(branchRule).css('color', color); + $(branchRule).css('background-color', color); +} + +FloMax.displayConditionRulesInput = function (branchRule) { + var conditionRulesInput = $(branchRule).find("input[name='conditionRules']"); + conditionRulesInput.removeClass('hidden'); +} + +FloMax.buildBranchRuleFromHTML = function (branchRule) { + //Get fromBranch by looking at the first column at this row. + var siblingCells = $(branchRule).parent().prevAll(); + var fromBranch = $(siblingCells).eq( ($(siblingCells).size())-1 ); //Get the first cell in this row. + fromBranch = $(fromBranch).html(); + //Get toBranch by looking at the first row of the current column. + var nthColumn = ($(siblingCells).size()); //Get x-position from all siblings + the column header, into this branchRule + var toBranch = $("#floatingMatrix").find("#fmHeaderRow").children("th").eq( nthColumn ).html(); + //Get floating + var floating = $(branchRule).find("select").val(); + //Get conditionRules + var conditionRules = $(branchRule).find("input[name='conditionRules']").val(); + //Get id + var id; + if ($(branchRule).attr('id')) { + id = $(branchRule).attr('id').substring(3); //Skip characters 'br_' + } + var brJSON = {'fromBranch' : fromBranch, + 'toBranch' : toBranch, + 'floating' : floating, + 'conditionRules' : conditionRules, + 'id' : id, + }; + return brJSON; +} +FloMax.storeBranchRule = function (branchRule) { + var brJSON = FloMax.buildBranchRuleFromHTML(branchRule); + FloMax.branchRules[brJSON.fromBranch+'-'+brJSON.toBranch] = brJSON; + FloMax.persistBranchRule(brJSON, branchRule); +} +FloMax.replaceBranchRule = function (branchRule) { + var brJSON = FloMax.buildBranchRuleFromHTML(branchRule); + var oldBrJSON = FloMax.branchRules[brJSON.fromBranch+'-'+brJSON.toBranch]; + var newBranchRule = FloMax.newBranchRuleHTML(oldBrJSON); + branchRule.replaceWith(newBranchRule); +} +FloMax.resetBranchRule = function (branchRule) { + branchRule.replaceWith(newBranchRuleHTML()); +} +FloMax.newBranchRuleHTML = function (branchRuleJSON) { + var branchRuleHTML = $("#br_TEMPLATE").clone('withDataAndElements'); + if (branchRuleJSON && branchRuleJSON.id) { + $(branchRuleHTML).attr('id','br_'+branchRuleJSON.id); + } + else { + $(branchRuleHTML).removeAttr('id'); + } + if (branchRuleJSON && branchRuleJSON.conditionRules) { + $(branchRuleHTML).find("input[name='conditionRules']").removeClass('hidden').val(branchRuleJSON.conditionRules); + } + else { + $(branchRuleHTML).find("input[name='conditionRules']").removeClass('hidden').val(''); + } + if (branchRuleJSON && branchRuleJSON.floating) { + $(branchRuleHTML).find("select").val( branchRuleJSON.floating || 'DISABLED' ); + } + else { + $(branchRuleHTML).find("select").val( 'DISABLED' ); + } + $(branchRuleHTML).removeClass('TEMPLATE'); + FloMax.changeBranchRuleColor(branchRuleHTML); + return branchRuleHTML; +} +/** + * var brJSON = FloMax.buildBranchRuleFromHTML(branchRule); + * FloMax.persistBranchRule(brJSON); + * + * INSERTs or UPDATEs or DELETEs the JSON:ified branchRule to the Koha DB + * @param {object} brJSON - JSON:ified object representation of a branchRule + * from buildBranchRuleFromHTML() + * @param {object} branchRule - the HTML element matching class ".branchRule". + * Used to target display modifications to it. + */ +FloMax.persistBranchRule = function (brJSON, branchRule) { + if (brJSON.floating == 'DISABLED') { + brJSON.delete = 1; //A hack to trick Perl CGI to understand this is a HTTP DELETE-verb. + $.ajax('floating-matrix-api.pl', + {method : 'POST', //Should be DELETE but damn CGI + data : brJSON, + dataType : 'json', + }).done(function(data, textStatus, jqXHR){ + FloMax.resetBranchRule(branchRule); + $(branchRule).removeClass('failedAjax'); + //alert("Saving floating rule "+brJSON.fromBranch+"-"+brJSON.toBranch+" OK, because of the following error:\n"+data.status+" "+data.statusText); + }).fail(function (data, textStatus, jqXHR) { + $(branchRule).addClass('failedAjax'); + var error = $.parseJSON(data.responseText); //Pass the error as JSON so we don't trigger the default Koha error pages. + alert("Deleting floating rule "+brJSON.fromBranch+"-"+brJSON.toBranch+" failed, because of the following error:\n"+data.status+" "+data.statusText+"\n"+"More specific error: "+error.error); + }); + } + else { + $.ajax('floating-matrix-api.pl', + {method : 'POST', + data : brJSON, + dataType : 'json', + }).done(function(data, textStatus, jqXHR){ + $(branchRule).removeClass('failedAjax'); + $(branchRule).attr('id', 'br_'+data.id); + }).fail(function (data, textStatus, jqXHR) { + $(branchRule).addClass('failedAjax'); + var error = $.parseJSON(data.responseText); //Pass the error as JSON so we don't trigger the default Koha error pages. + alert("Saving floating rule "+brJSON.fromBranch+"-"+brJSON.toBranch+" failed, because of the following error:\n"+data.status+" "+data.statusText+"\n"+"More specific error: "+error.error); + }); + } +} + +FloMax.Tester.defaultTestResultColor = "#B9D8D9"; + +FloMax.Tester.testFloating = function() { + var testCase = FloMax.Tester.buildTestCaseFromHTML(); + $(".cssload-loader").css('visibility', 'visible'); + + $.ajax('floating-matrix-api.pl', + {method : 'POST', + data : testCase, + dataType : 'json', + }).done(function(data, textStatus, jqXHR){ + + FloMax.Tester.displayTestResult(data.testResult); + + }).fail(function (data, textStatus, jqXHR) { + var error = $.parseJSON(data.responseText); //Pass the error as JSON so we don't trigger the default Koha error pages. + alert("Testing floating rule "+testCase.fromBranch+"-"+testCase.toBranch+" failed, because of the following error:\n"+data.status+" "+data.statusText+"\n"+"More specific error: "+error.error); + FloMax.Tester.displayTestResult("error"); + }); +} +FloMax.Tester.buildTestCaseFromHTML = function () { + var fromBranch = $("#floatingRuleTester #testerFromBranch").val(); + var toBranch = $("#floatingRuleTester #testerToBranch").val(); + var barcode = $("#floatingRuleTester #testerBarcode").val(); + + var testCase = { + 'fromBranch' : fromBranch, + 'toBranch' : toBranch, + 'barcode' : barcode, + 'test': true, + }; + return testCase; +} +FloMax.Tester.displayTestResult = function (testResult) { + $(".cssload-loader").css('visibility', 'hidden'); + var color; + if (testResult == null) { + color = FloMax.branchRuleDisabledColor; + } + else if (testResult == 'ALWAYS') { + color = FloMax.branchRuleAlwaysColor; + } + else if (testResult == 'POSSIBLE') { + color = FloMax.branchRulePossibleColor; + } + else if (testResult == "error") { + color = FloMax.Tester.defaultTestResultColor; + } + else { + color = FloMax.Tester.defaultTestResultColor; + alert("Couldn't display test result '"+testResult+"'. Value is unknown."); + } + + $("#testResulDisplay").css('background-color', color); +} \ No newline at end of file -- 2.17.1