use OpenSRF::Utils::Cache;
use OpenSRF::Utils::Logger qw/$logger/;
use OpenILS::Utils::ModsParser;
+use OpenSRF::Utils qw/:datetime/;
use OpenSRF::EX qw(:try);
use OpenILS::Event;
use Data::Dumper;
use Encode;
use DateTime;
use DateTime::Format::ISO8601;
+use DateTime::SpanSet;
use List::MoreUtils qw/uniq/;
use Digest::MD5 qw(md5_hex);
}
+# Returns a date representing the final day in a series of
+# days that include the desired number of open days for the
+# requested org unit.
+#
+# The calculation starts at noon tomorrow and counts forward
+# until enough open days have been found to span the selected
+# day count.
+#
+# For example, if today was Jan 1 and a day count of 3 was
+# requested, assuming no closed date or hours of operation
+# collisions, the API would return Jan 4.
+sub org_unit_open_days {
+ my ($class, $org_id, $day_count, $e) = @_;
+ $e ||= OpenILS::Utils::CStoreEditor->new;
+
+ my $spanset;
+ my $date = DateTime->now(time_zone => 'local'); # TODO: org tz
+ my $hoo = $e->retrieve_actor_org_unit_hours_of_operation($org_id);
+
+ my $counter = 0;
+ while ($counter++ < $day_count) {
+
+ $date->set(hour => '12', minute => '0', second => '0');
+ $date->add(days => 1);
+ my $date_str = $date->strftime('%FT%T%z');
+
+ $spanset = org_closed_future_spanset($e, $org_id, $date_str) unless $spanset;
+
+ # method_lookup is the preferred way to invoke in-module
+ # API calls, but in this case method_lookup is an order
+ # of magnitude slower with high day counts :(.
+ my $end = org_next_open_day($org_id, $date_str, $spanset, $hoo);
+
+ # No overlap means today is good. If we found an overlap
+ # push the current test date to the end date of the overlap.
+ # This new date is also known good.
+
+ if ($end) {
+ $logger->info("open days calc found an overlap ending at $end");
+ $date = DateTime::Format::ISO8601
+ ->parse_datetime(cleanse_ISO8601($end));
+ }
+ }
+
+ return $date->strftime('%FT%T%z');
+}
+
+# Build a spanset of dates starting at 'date' including all future
+# closed dates.
+sub org_closed_future_spanset {
+ my ($e, $org_id, $date) = @_;
+
+ $logger->debug(
+ "Creating org closed dates spanset for $org_id starting $date");
+
+ my $close_dates = $e->search_actor_org_unit_closed_date(
+ {org_unit => $org_id, close_end => {'>=' => $date}});
+
+ return undef unless @$close_dates;
+
+ my $dtp = DateTime::Format::ISO8601->new;
+
+ my $spanset = DateTime::SpanSet->empty_set;
+ for my $close (@$close_dates) {
+ my $start = $dtp->parse_datetime(cleanse_ISO8601($close->close_start));
+ my $end = $dtp->parse_datetime(cleanse_ISO8601($close->close_end));
+
+ $spanset = $spanset->union(
+ DateTime::Span->new(start => $start, end => $end));
+ }
+
+ return $spanset;
+}
+
+# Returns a date string representing the next day the selected org
+# unit is open, taking both closed dates and hours of operation info
+# account.
+sub org_next_open_day {
+ my $org_id = shift;
+ my $date = shift;
+ my $closure_spanset = shift;
+ my $hoo = shift;
+ my $dtp = DateTime::Format::ISO8601->new;
+
+ $date = cleanse_ISO8601($date);
+ my $target_date = $dtp->parse_datetime($date);
+ my ($begin, $end) = ($target_date, $target_date);
+
+ if ($closure_spanset && $closure_spanset->intersects($target_date)) {
+ my $intersection = $closure_spanset->intersection( $target_date );
+ $begin = $intersection->min;
+ $end = $intersection->max;
+
+ $end->add(days => 1);
+ while (my $next = org_next_open_day($org_id,
+ $end->strftime('%FT%T%z'), $closure_spanset, $hoo)) {
+ $end = $dtp->parse_datetime(cleanse_ISO8601($next));
+ }
+ }
+
+ # Look for the next open hours-of-operation day. If no open day is
+ # found after 7 attempts, give up to avoid looping on orgs that
+ # have no open hours of operation.
+ my $hoo_counter = 0;
+ my $hoo_date = $end->clone;
+ while ($hoo_counter++ < 7) {
+ if (org_operates_on_date($org_id, $hoo_date, $hoo)) {
+ $end = $hoo_date;
+ last;
+ }
+ $hoo_date->add(days => 1);
+ }
+
+ if ($hoo_counter == 7) {
+ # The org unit is closed every day, which means this
+ # function can never return a meaningful value.
+ $logger->warn("Org unit $org_id is closed every day");
+ return undef;
+ }
+
+ my $start = $begin->strftime('%FT%T%z');
+ my $stop = $end->strftime('%FT%T%z');
+
+ # If the start and end date match, no date modifications were
+ # required. Selected date is open.
+ return undef if ($start eq $stop);
+
+ if ($hoo_counter > 1) {
+ # End date pushed ahead due to an hours of operation collision.
+ # Confirm the new date is not a closed date.
+
+ my $final_date = org_next_open_day(
+ $org_id, $stop, $closure_spanset, $hoo);
+
+ # date was bumped ahead even further.
+ return $final_date if $final_date;
+ }
+
+ return $stop;
+}
+
+# Returns true of the org unit has any hours of operation
+# for the selected date.
+sub org_operates_on_date {
+ my ($org_id, $date, $hoo) = @_;
+
+ # No dates means always open
+ return 1 unless $hoo;
+
+ my $dow = $date->day_of_week_0;
+ my $omethod = "dow_${dow}_open";
+ my $cmethod = "dow_${dow}_close";
+
+ # both values will equal 0 when the org unit is closed.
+ return $hoo->$omethod() ne $hoo->$cmethod();
+}
+
1;
sub set_hold_shelf_expire_time {
my ($class, $hold, $editor, $start_time) = @_;
+ # TODO: create org unit setting for explicitly tracking days.
my $shelf_expire = $U->ou_ancestor_setting_value(
$hold->pickup_lib,
'circ.holds.default_shelf_expire_interval',
return undef unless $shelf_expire;
- $start_time = ($start_time) ?
- DateTime::Format::ISO8601->new->parse_datetime(clean_ISO8601($start_time)) :
- DateTime->now(time_zone => 'local'); # without time_zone we get UTC ... yuck!
-
- my $seconds = OpenILS::Utils::DateTime->interval_to_seconds($shelf_expire);
- my $expire_time = $start_time->add(seconds => $seconds);
+ # Grab the numeric interval from the text
+ # TODO: THIS MAKES ASSUMPTIONS ABOUT THE EXPIRE INTERVAL
+ # BEING DEFINED IN DAYS.
+ # TODO: See above about shelf_expire_days setting
+ my $day_count;
+ if ($shelf_expire =~ /(\d+)/g) {
+ $day_count = $1;
+ }
- # if the shelf expire time overlaps with a pickup lib's
- # closed date, push it out to the first open date
- my $dateinfo = $U->storagereq(
- 'open-ils.storage.actor.org_unit.closed_date.overlap',
- $hold->pickup_lib, $expire_time->strftime('%FT%T%z'));
+ return undef unless $day_count;
- if($dateinfo) {
- my $dt_parser = DateTime::Format::ISO8601->new;
- $expire_time = $dt_parser->parse_datetime(clean_ISO8601($dateinfo->{end}));
+ my $date_str = $U->org_unit_open_days(
+ $hold->pickup_lib, $day_count, $editor);
- # TODO: enable/disable time bump via setting?
- $expire_time->set(hour => '23', minute => '59', second => '59');
-
- $logger->info("circulator: shelf_expire_time overlaps".
- " with closed date, pushing expire time to $expire_time");
- }
+ my $expire_time = DateTime::Format::ISO8601->new
+ ->parse_datetime(clean_ISO8601($date_str));
+ # Hold shelf expire should include the full final day.
+ # TODO: enable/disable time bump via setting?
+ $expire_time->set(hour => '23', minute => '59', second => '59');
$hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
+
return undef;
}