Merge branch 'master' of git.evergreen-ils.org:Evergreen-DocBook into doc_consolidati...
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Circ / CircCommon.pm
1 package OpenILS::Application::Circ::CircCommon;
2 use strict; use warnings;
3 use DateTime;
4 use DateTime::Format::ISO8601;
5 use OpenILS::Application::AppUtils;
6 use OpenSRF::Utils qw/:datetime/;
7 use OpenILS::Event;
8 use OpenSRF::Utils::Logger qw(:logger);
9 use OpenILS::Utils::CStoreEditor q/:funcs/;
10 use OpenILS::Const qw/:const/;
11
12 my $U = "OpenILS::Application::AppUtils";
13
14 # -----------------------------------------------------------------
15 # Do not publish methods here.  This code is shared across apps.
16 # -----------------------------------------------------------------
17
18
19 # -----------------------------------------------------------------
20 # Voids overdue fines on the given circ.  if a backdate is 
21 # provided, then we only void back to the backdate, unless the
22 # backdate is to within the grace period, in which case we void all
23 # overdue fines.
24 # -----------------------------------------------------------------
25 sub void_overdues {
26     my($class, $e, $circ, $backdate, $note) = @_;
27
28     my $bill_search = { 
29         xact => $circ->id, 
30         btype => 1 
31     };
32
33     if( $backdate ) {
34         # ------------------------------------------------------------------
35         # Fines for overdue materials are assessed up to, but not including,
36         # one fine interval after the fines are applicable.  Here, we add
37         # one fine interval to the backdate to ensure that we are not 
38         # voiding fines that were applicable before the backdate.
39         # ------------------------------------------------------------------
40
41         # if there is a raw time component (e.g. from postgres), 
42         # turn it into an interval that interval_to_seconds can parse
43         my $duration = $circ->fine_interval;
44         $duration =~ s/(\d{2}):(\d{2}):(\d{2})/$1 h $2 m $3 s/o;
45         my $interval = OpenSRF::Utils->interval_to_seconds($duration);
46
47         my $date = DateTime::Format::ISO8601->parse_datetime($backdate);
48         my $due_date = DateTime::Format::ISO8601->parse_datetime(cleanse_ISO8601($circ->due_date))->epoch;
49         my $grace_period = extend_grace_period( $class, $circ->circ_lib, $circ->due_date, OpenSRF::Utils->interval_to_seconds($circ->grace_period), $e);
50         if($date->epoch <= $due_date + $grace_period) {
51             $logger->info("backdate $backdate is within grace period, voiding all");
52         } else {
53             $backdate = $U->epoch2ISO8601($date->epoch + $interval);
54             $logger->info("applying backdate $backdate in overdue voiding");
55             $$bill_search{billing_ts} = {'>=' => $backdate};
56         }
57     }
58
59     my $bills = $e->search_money_billing($bill_search);
60     
61     for my $bill (@$bills) {
62         next if $U->is_true($bill->voided);
63         $logger->info("voiding overdue bill ".$bill->id);
64         $bill->voided('t');
65         $bill->void_time('now');
66         $bill->voider($e->requestor->id);
67         my $n = ($bill->note) ? sprintf("%s\n", $bill->note) : "";
68         $bill->note(sprintf("$n%s", ($note) ? $note : "System: VOIDED FOR BACKDATE"));
69         $e->update_money_billing($bill) or return $e->die_event;
70     }
71
72         return undef;
73 }
74
75
76 sub reopen_xact {
77     my($class, $e, $xactid) = @_;
78
79     # -----------------------------------------------------------------
80     # make sure the transaction is not closed
81     my $xact = $e->retrieve_money_billable_transaction($xactid)
82         or return $e->die_event;
83
84     if( $xact->xact_finish ) {
85         my ($mbts) = $U->fetch_mbts($xactid, $e);
86         if( $mbts->balance_owed != 0 ) {
87             $logger->info("* re-opening xact $xactid, orig xact_finish is ".$xact->xact_finish);
88             $xact->clear_xact_finish;
89             $e->update_money_billable_transaction($xact)
90                 or return $e->die_event;
91         } 
92     }
93
94     return undef;
95 }
96
97
98 sub create_bill {
99         my($class, $e, $amount, $btype, $type, $xactid, $note) = @_;
100
101         $logger->info("The system is charging $amount [$type] on xact $xactid");
102     $note ||= 'SYSTEM GENERATED';
103
104     # -----------------------------------------------------------------
105     # now create the billing
106         my $bill = Fieldmapper::money::billing->new;
107         $bill->xact($xactid);
108         $bill->amount($amount);
109         $bill->billing_type($type); 
110         $bill->btype($btype); 
111         $bill->note($note);
112     $e->create_money_billing($bill) or return $e->die_event;
113
114         return undef;
115 }
116
117 sub extend_grace_period {
118     my($class, $circ_lib, $due_date, $grace_period, $e, $h) = @_;
119     if ($grace_period >= 86400) { # Only extend grace periods greater than or equal to a full day
120         my $parser = DateTime::Format::ISO8601->new;
121         my $due_dt = $parser->parse_datetime( cleanse_ISO8601( $due_date ) );
122         my $due = $due_dt->epoch;
123
124         my $grace_extend = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend');
125         $e = new_editor() if (!$e);
126         $h = $e->retrieve_actor_org_unit_hours_of_operation($circ_lib) if (!$h);
127         if ($grace_extend and $h) { 
128             my $new_grace_period = $grace_period;
129
130             $logger->info( "Circ lib has an hours-of-operation entry and grace period extension is enabled." );
131
132             my $closed = 0;
133             my %h_closed = {};
134             for my $i (0 .. 6) {
135                 my $dow_open = "dow_${i}_open";
136                 my $dow_close = "dow_${i}_close";
137                 if($h->$dow_open() eq '00:00:00' and $h->$dow_close() eq '00:00:00') {
138                     $closed++;
139                     $h_closed{$i} = 1;
140                 } else {
141                     $h_closed{$i} = 0;
142                 }
143             }
144
145             if($closed == 7) {
146                 $logger->info("Circ lib is closed all week according to hours-of-operation entry. Skipping grace period extension checks.");
147             } else {
148                 # Extra nice grace periods
149                 # AKA, merge closed dates trailing the grace period into the grace period
150                 my $grace_extend_into_closed = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.into_closed');
151                 $due += 86400 if $grace_extend_into_closed;
152
153                 my $grace_extend_all = $U->ou_ancestor_setting_value($circ_lib, 'circ.grace.extend.all');
154
155                 if ( $grace_extend_all ) {
156                     # Start checking the day after the item was due
157                     # This is "The grace period only counts open days"
158                     # NOTE: Adding 86400 seconds is not the same as adding one day. This uses seconds intentionally.
159                     $due_dt = $due_dt->add( seconds => 86400 );
160                 } else {
161                     # Jump to the end of the grace period
162                     # This is "If the grace period ends on a closed day extend it"
163                     # NOTE: This adds grace period as a number of seconds intentionally
164                     $due_dt = $due_dt->add( seconds => $grace_period );
165                 }
166
167                 my $count = 0; # Infinite loop protection
168                 do {
169                     $closed = 0; # Starting assumption for day: We are not closed
170                     $count++; # We limit the number of loops below.
171
172                     # get the day of the week for the day we are looking at
173                     my $dow = $due_dt->day_of_week_0;
174
175                     # Check hours of operation first.
176                     if ($h_closed{$dow}) {
177                         $closed = 1;
178                         $new_grace_period += 86400;
179                         $due_dt->add( seconds => 86400 );
180                     } else {
181                         # Check for closed dates for this period
182                         my $timestamptz = $due_dt->strftime('%FT%T%z');
183                         my $cl = $e->search_actor_org_unit_closed_date(
184                                 { close_start => { '<=' => $timestamptz },
185                                   close_end   => { '>=' => $timestamptz },
186                                   org_unit    => $circ_lib }
187                         );
188                         if ($cl and @$cl) {
189                             $closed = 1;
190                             foreach (@$cl) {
191                                 my $cl_dt = $parser->parse_datetime( cleanse_ISO8601( $_->close_end ) );
192                                 while ($due_dt <= $cl_dt) {
193                                     $due_dt->add( seconds => 86400 );
194                                     $new_grace_period += 86400;
195                                 }
196                             }
197                         } else {
198                             $due_dt->add( seconds => 86400 );
199                         }
200                     }
201                 } while ( $count <= 366 and ( $closed or $due_dt->epoch <= $due + $new_grace_period ) );
202                 if ($new_grace_period > $grace_period) {
203                     $grace_period = $new_grace_period;
204                     $logger->info( "Grace period for circ extended to $grace_period [" . seconds_to_interval( $grace_period ) . "]" );
205                 }
206             }
207         }
208     }
209     return $grace_period;
210 }
211
212 1;