4dc88af99369cc175b7a9b73001510a9902457b0
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Acq / Order.pm
1 package OpenILS::Application::Acq::BatchManager;
2 use OpenILS::Application::Acq::Financials;
3 use OpenSRF::AppSession;
4 use OpenSRF::EX qw/:try/;
5 use strict; use warnings;
6
7 sub new {
8     my($class, %args) = @_;
9     my $self = bless(\%args, $class);
10     $self->{args} = {
11         lid => 0,
12         li => 0,
13         vqbr => 0,
14         copies => 0,
15         bibs => 0,
16         progress => 0,
17         debits_accrued => 0,
18         purchase_order => undef,
19         picklist => undef,
20         complete => 0,
21         indexed => 0,
22         queue => undef,
23         total => 0
24     };
25     $self->{cache} = {};
26     $self->throttle(4) unless $self->throttle;
27     $self->{post_proc_queue} = [];
28     $self->{last_respond_progress} = 0;
29     return $self;
30 }
31
32 sub conn {
33     my($self, $val) = @_;
34     $self->{conn} = $val if $val;
35     return $self->{conn};
36 }
37 sub throttle {
38     my($self, $val) = @_;
39     $self->{throttle} = $val if $val;
40     return $self->{throttle};
41 }
42 sub respond {
43     my($self, %other_args) = @_;
44     if($self->throttle and not %other_args) {
45         return unless (
46             ($self->{args}->{progress} - $self->{last_respond_progress}) >= $self->throttle
47         );
48     }
49     $self->conn->respond({ %{$self->{args}}, %other_args });
50     $self->{last_respond_progress} = $self->{args}->{progress};
51     $self->throttle($self->throttle * 2) unless $self->throttle >= 256;
52 }
53 sub respond_complete {
54     my($self, %other_args) = @_;
55     $self->complete;
56     $self->conn->respond_complete({ %{$self->{args}}, %other_args });
57     $self->run_post_response_hooks;
58     return undef;
59 }
60
61 # run the post response hook subs, shifting them off as we go
62 sub run_post_response_hooks {
63     my($self) = @_;
64     (shift @{$self->{post_proc_queue}})->() while @{$self->{post_proc_queue}};
65 }
66
67 # any subs passed to this method will be run after the call to respond_complete
68 sub post_process {
69     my($self, $sub) = @_;
70     push(@{$self->{post_proc_queue}}, $sub);
71 }
72
73 sub total {
74     my($self, $val) = @_;
75     $self->{args}->{total} = $val if defined $val;
76     $self->{args}->{maximum} = $self->{args}->{total};
77     return $self->{args}->{total};
78 }
79 sub purchase_order {
80     my($self, $val) = @_;
81     $self->{args}->{purchase_order} = $val if $val;
82     return $self;
83 }
84 sub picklist {
85     my($self, $val) = @_;
86     $self->{args}->{picklist} = $val if $val;
87     return $self;
88 }
89 sub add_lid {
90     my $self = shift;
91     $self->{args}->{lid} += 1;
92     $self->{args}->{progress} += 1;
93     return $self;
94 }
95 sub add_li {
96     my $self = shift;
97     $self->{args}->{li} += 1;
98     $self->{args}->{progress} += 1;
99     return $self;
100 }
101 sub add_vqbr {
102     my $self = shift;
103     $self->{args}->{vqbr} += 1;
104     $self->{args}->{progress} += 1;
105     return $self;
106 }
107 sub add_copy {
108     my $self = shift;
109     $self->{args}->{copies} += 1;
110     $self->{args}->{progress} += 1;
111     return $self;
112 }
113 sub add_bib {
114     my $self = shift;
115     $self->{args}->{bibs} += 1;
116     $self->{args}->{progress} += 1;
117     return $self;
118 }
119 sub add_debit {
120     my($self, $amount) = @_;
121     $self->{args}->{debits_accrued} += $amount;
122     $self->{args}->{progress} += 1;
123     return $self;
124 }
125 sub editor {
126     my($self, $editor) = @_;
127     $self->{editor} = $editor if defined $editor;
128     return $self->{editor};
129 }
130 sub complete {
131     my $self = shift;
132     $self->{args}->{complete} = 1;
133     return $self;
134 }
135
136 sub cache {
137     my($self, $org, $key, $val) = @_;
138     $self->{cache}->{$org} = {} unless $self->{cache}->{org};
139     $self->{cache}->{$org}->{$key} = $val if defined $val;
140     return $self->{cache}->{$org}->{$key};
141 }
142
143
144 package OpenILS::Application::Acq::Order;
145 use base qw/OpenILS::Application/;
146 use strict; use warnings;
147 # ----------------------------------------------------------------------------
148 # Break up each component of the order process and pieces into managable
149 # actions that can be shared across different workflows
150 # ----------------------------------------------------------------------------
151 use OpenILS::Event;
152 use OpenSRF::Utils::Logger qw(:logger);
153 use OpenSRF::Utils::JSON;
154 use OpenSRF::AppSession;
155 use OpenILS::Utils::Fieldmapper;
156 use OpenILS::Utils::CStoreEditor q/:funcs/;
157 use OpenILS::Utils::Normalize qw/clean_marc/;
158 use OpenILS::Const qw/:const/;
159 use OpenSRF::EX q/:try/;
160 use OpenILS::Application::AppUtils;
161 use OpenILS::Application::Cat::BibCommon;
162 use OpenILS::Application::Cat::AssetCommon;
163 use MARC::Record;
164 use MARC::Batch;
165 use MARC::File::XML (BinaryEncoding => 'UTF-8');
166 use Digest::MD5 qw(md5_hex);
167 use Data::Dumper;
168 $Data::Dumper::Indent = 0;
169 my $U = 'OpenILS::Application::AppUtils';
170
171
172 # ----------------------------------------------------------------------------
173 # Lineitem
174 # ----------------------------------------------------------------------------
175 sub create_lineitem {
176     my($mgr, %args) = @_;
177     my $li = Fieldmapper::acq::lineitem->new;
178     $li->creator($mgr->editor->requestor->id);
179     $li->selector($li->creator);
180     $li->editor($li->creator);
181     $li->create_time('now');
182     $li->edit_time('now');
183     $li->state('new');
184     $li->$_($args{$_}) for keys %args;
185     $li->clear_id;
186     $mgr->add_li;
187     $mgr->editor->create_acq_lineitem($li) or return 0;
188     
189     unless($li->estimated_unit_price) {
190         # extract the price from the MARC data
191         my $price = get_li_price_from_attr($mgr->editor, $li) or return $li;
192         $li->estimated_unit_price($price);
193         return update_lineitem($mgr, $li);
194     }
195
196     return $li;
197 }
198
199 sub get_li_price_from_attr {
200     my($e, $li) = @_;
201     my $attrs = $li->attributes || $e->search_acq_lineitem_attr({lineitem => $li->id});
202
203     for my $attr_type (qw/    
204             lineitem_local_attr_definition 
205             lineitem_prov_attr_definition 
206             lineitem_marc_attr_definition/) {
207
208         my ($attr) = grep {
209             $_->attr_name eq 'estimated_price' and 
210             $_->attr_type eq $attr_type } @$attrs;
211
212         return $attr->attr_value if $attr;
213     }
214
215     return undef;
216 }
217
218
219 sub update_lineitem {
220     my($mgr, $li) = @_;
221     $li->edit_time('now');
222     $li->editor($mgr->editor->requestor->id);
223     $mgr->add_li;
224     return $mgr->editor->retrieve_acq_lineitem($mgr->editor->data) if
225         $mgr->editor->update_acq_lineitem($li);
226     return undef;
227 }
228
229
230 # ----------------------------------------------------------------------------
231 # Create real holds from patron requests for a given lineitem
232 # ----------------------------------------------------------------------------
233 sub promote_lineitem_holds {
234     my($mgr, $li) = @_;
235
236     my $requests = $mgr->editor->search_acq_user_request(
237         { lineitem => $li->id,
238           '-or' =>
239             [ { need_before => {'>' => 'now'} },
240               { need_before => undef }
241             ]
242         }
243     );
244
245     for my $request ( @$requests ) {
246
247         $request->eg_bib( $li->eg_bib_id );
248         $mgr->editor->update_acq_user_request( $request ) or return 0;
249
250         next unless ($U->is_true( $request->hold ));
251
252         my $hold = Fieldmapper::action::hold_request->new;
253         $hold->usr( $request->usr );
254         $hold->requestor( $request->usr );
255         $hold->request_time( $request->request_date );
256         $hold->pickup_lib( $request->pickup_lib );
257         $hold->request_lib( $request->pickup_lib );
258         $hold->selection_ou( $request->pickup_lib );
259         $hold->phone_notify( $request->phone_notify );
260         $hold->email_notify( $request->email_notify );
261         $hold->expire_time( $request->need_before );
262
263         if ($request->holdable_formats) {
264             my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
265             if ($mrm) {
266                 $hold->hold_type( 'M' );
267                 $hold->holdable_formats( $request->holdable_formats );
268                 $hold->target( $mrm->metarecord );
269             }
270         }
271
272         if (!$hold->target) {
273             $hold->hold_type( 'T' );
274             $hold->target( $li->eg_bib_id );
275         }
276
277         # if behind-the-desk holds are supported at the 
278         # pickup library, apply the patron default
279         my $bdous = $U->ou_ancestor_setting_value(
280             $hold->pickup_lib, 
281             'circ.holds.behind_desk_pickup_supported', 
282             $mgr->editor
283         );
284
285         if ($bdous) {
286             my $set = $mgr->editor->search_actor_user_setting(
287                 {usr => $hold->usr, name => 'circ.holds_behind_desk'})->[0];
288     
289             $hold->behind_desk('t') if $set and 
290                 OpenSRF::Utils::JSON->JSON2perl($set->value);
291         }
292
293         $mgr->editor->create_action_hold_request( $hold ) or return 0;
294     }
295
296     return $li;
297 }
298
299 sub delete_lineitem {
300     my($mgr, $li) = @_;
301     $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
302
303     # delete the attached lineitem_details
304     my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
305     for my $lid_id (@$lid_ids) {
306         return 0 unless delete_lineitem_detail($mgr, $lid_id);
307     }
308
309     $mgr->add_li;
310     return $mgr->editor->delete_acq_lineitem($li);
311 }
312
313 # begins and commit transactions as it goes
314 # bib_only exits before creation of copies and callnumbers
315 sub create_lineitem_list_assets {
316     my($mgr, $li_ids, $vandelay, $bib_only) = @_;
317
318     # Do not create line items if none are specified
319     return {} unless (scalar(@$li_ids));
320
321     if (check_import_li_marc_perms($mgr, $li_ids)) { # event on error
322         $logger->error("acq-vl: user does not have permission to import acq records");
323         return undef;
324     }
325
326     my $res = import_li_bibs_via_vandelay($mgr, $li_ids, $vandelay);
327     return undef unless $res;
328     return $res if $bib_only;
329
330     # create the bibs/volumes/copies for the successfully imported records
331     for my $li_id (@{$res->{li_ids}}) {
332         $mgr->editor->xact_begin;
333         my $data = create_lineitem_assets($mgr, $li_id) or return undef;
334         $mgr->editor->xact_commit;
335         $mgr->respond;
336     }
337
338     return $res;
339 }
340
341 sub test_vandelay_import_args {
342     my $vandelay = shift;
343     my $q_needed = shift;
344
345     # we need valid args and (sometimes) a queue
346     return 0 unless $vandelay and (
347         !$q_needed or
348         $vandelay->{queue_name} or 
349         $vandelay->{existing_queue}
350     );
351
352     # match-based merge/overlay import
353     return 2 if $vandelay->{merge_profile} and (
354         $vandelay->{auto_overlay_exact} or
355         $vandelay->{auto_overlay_1match} or
356         $vandelay->{auto_overlay_best_match}
357     );
358
359     # no-match import
360     return 2 if $vandelay->{import_no_match};
361
362     return 1; # queue only
363 }
364
365 sub find_or_create_vandelay_queue {
366     my ($e, $vandelay) = @_;
367
368     my $queue;
369     if (my $name = $vandelay->{queue_name}) {
370
371         # first, see if a queue w/ this name already exists
372         # for this user.  If so, use that instead.
373
374         $queue = $e->search_vandelay_bib_queue(
375             {name => $name, owner => $e->requestor->id})->[0];
376
377         if ($queue) {
378
379             $logger->info("acq-vl: using existing queue $name");
380
381         } else {
382
383             $logger->info("acq-vl: creating new vandelay queue $name");
384
385             $queue = new Fieldmapper::vandelay::bib_queue;
386             $queue->name($name); 
387             $queue->queue_type('acq');
388             $queue->owner($e->requestor->id);
389             $queue->match_set($vandelay->{match_set} || undef); # avoid ''
390             $queue = $e->create_vandelay_bib_queue($queue) or return undef;
391         }
392
393     } else {
394         $queue = $e->retrieve_vandelay_bib_queue($vandelay->{existing_queue})
395             or return undef;
396     }
397     
398     return $queue;
399 }
400
401
402 sub import_li_bibs_via_vandelay {
403     my ($mgr, $li_ids, $vandelay) = @_;
404     my $res = {li_ids => []};
405     my $e = $mgr->editor;
406     $e->xact_begin;
407
408     my $needs_importing = $e->search_acq_lineitem(
409         {id => $li_ids, eg_bib_id => undef}, 
410         {idlist => 1}
411     );
412
413     if (!@$needs_importing) {
414         $logger->info("acq-vl: all records already imported.  no Vandelay work to do");
415         return {li_ids => $li_ids};
416     }
417
418     # see if we have any records that are not yet linked to VL records (i.e. 
419     # not in a queue).  This will tell us if lack of a queue name is an error.
420     my $non_queued = $e->search_acq_lineitem(
421         {id => $needs_importing, queued_record => undef},
422         {idlist => 1}
423     );
424
425     # add the already-imported records to the response list
426     push(@{$res->{li_ids}}, grep { $_ != @$needs_importing } @$li_ids);
427
428     $logger->info("acq-vl: processing recs via Vandelay with args: ".Dumper($vandelay));
429
430     my $vl_stat = test_vandelay_import_args($vandelay, scalar(@$non_queued));
431     if ($vl_stat == 0) {
432         $logger->error("acq-vl: invalid vandelay arguments for acq import (queue needed)");
433         return $res;
434     }
435
436     my $queue;
437     if (@$non_queued) {
438         # when any non-queued lineitems exist, their vandelay counterparts 
439         # require a place to live.
440         $queue = find_or_create_vandelay_queue($e, $vandelay) or return $res;
441
442     } else {
443         # if all lineitems are already queued, the queue reported to the user
444         # is purely for information / convenience.  pick a random queue.
445         $queue = $e->retrieve_acq_lineitem([
446             $needs_importing->[0], {   
447                 flesh => 2, 
448                 flesh_fields => {
449                     jub => ['queued_record'], 
450                     vqbr => ['queue']
451                 }
452             }
453         ])->queued_record->queue;
454     }
455
456     $mgr->{args}->{queue} = $queue;
457
458     # load the lineitems into the queue for merge processing
459     my @vqbr_ids;
460     my @lis;
461     for my $li_id (@$needs_importing) {
462
463         my $li = $e->retrieve_acq_lineitem($li_id) or return $res;
464
465         if ($li->queued_record) {
466             $logger->info("acq-vl: $li_id already linked to a vandelay record");
467             push(@vqbr_ids, $li->queued_record);
468
469         } else {
470             $logger->info("acq-vl: creating new vandelay record for lineitem $li_id");
471
472             # create a new VL queued record and link it up
473             my $vqbr = Fieldmapper::vandelay::queued_bib_record->new;
474             $vqbr->marc($li->marc);
475             $vqbr->queue($queue->id);
476             $vqbr->bib_source($vandelay->{bib_source} || undef); # avoid ''
477             $vqbr = $e->create_vandelay_queued_bib_record($vqbr) or return $res;
478             push(@vqbr_ids, $vqbr->id);
479
480             # tell the acq record which vandelay record it's linked to
481             $li->queued_record($vqbr->id);
482             $e->update_acq_lineitem($li) or return $res;
483         }
484
485         $mgr->add_vqbr;
486         $mgr->respond;
487         push(@lis, $li);
488     }
489
490     $logger->info("acq-vl: created vandelay records [@vqbr_ids]");
491
492     # we have to commit the transaction now since 
493     # vandelay uses its own transactions.
494     $e->commit;
495
496     return $res if $vl_stat == 1; # queue only
497
498     # Import the bibs via vandelay.  Note: Vandely will 
499     # update acq.lineitem.eg_bib_id on successful import.
500
501     $vandelay->{report_all} = 1;
502     my $ses = OpenSRF::AppSession->create('open-ils.vandelay');
503     my $req = $ses->request(
504         'open-ils.vandelay.bib_record.list.import',
505         $e->authtoken, \@vqbr_ids, $vandelay);
506
507     # pull the responses, noting all that were successfully imported
508     my @success_lis;
509     while (my $resp = $req->recv(timeout => 600)) {
510         my $stat = $resp->content;
511
512         if(!$stat or $U->event_code($stat)) { # import failure
513             $logger->error("acq-vl: error importing vandelay record " . Dumper($stat));
514             next;
515         }
516
517         # "imported" refers to the vqbr id, not the 
518         # success/failure of the vqbr merge attempt
519         next unless $stat->{imported};
520
521         my ($imported) = grep {$_->queued_record eq $stat->{imported}} @lis;
522         my $li_id = $imported->id;
523
524         if ($stat->{no_import}) {
525             $logger->info("acq-vl: acq lineitem $li_id did not import"); 
526
527         } else { # successful import
528
529             push(@success_lis, $li_id);
530             $mgr->add_bib;
531             $mgr->respond;
532             $logger->info("acq-vl: acq lineitem $li_id successfully merged/imported");
533         } 
534     }
535
536     $ses->kill_me;
537     $logger->info("acq-vl: successfully imported lineitems [@success_lis]");
538
539     # add the successfully imported lineitems to the already-imported lineitems
540     push (@{$res->{li_ids}}, @success_lis);
541
542     return $res;
543 }
544
545 # returns event on error, undef on success
546 sub check_import_li_marc_perms {
547     my($mgr, $li_ids) = @_;
548
549     # if there are any order records that are not linked to 
550     # in-db bib records, verify staff has perms to import order records
551     my $order_li = $mgr->editor->search_acq_lineitem(
552         [{id => $li_ids, eg_bib_id => undef}, {limit => 1}], {idlist => 1})->[0];
553
554     if($order_li) {
555         return $mgr->editor->die_event unless 
556             $mgr->editor->allowed('IMPORT_ACQ_LINEITEM_BIB_RECORD');
557     }
558
559     return undef;
560 }
561
562
563 # ----------------------------------------------------------------------------
564 # if all of the lineitem details for this lineitem have 
565 # been received, mark the lineitem as received
566 # returns 1 on non-received, li on received, 0 on error
567 # ----------------------------------------------------------------------------
568
569 sub describe_affected_po {
570     my ($e, $po) = @_;
571
572     my ($enc, $spent) =
573         OpenILS::Application::Acq::Financials::build_price_summary(
574             $e, $po->id
575         );
576
577     +{$po->id => {
578             "state" => $po->state,
579             "amount_encumbered" => $enc,
580             "amount_spent" => $spent
581         }
582     };
583 }
584
585 sub check_lineitem_received {
586     my($mgr, $li_id) = @_;
587
588     my $non_recv = $mgr->editor->search_acq_lineitem_detail(
589         {recv_time => undef, lineitem => $li_id}, {idlist=>1});
590
591     return 1 if @$non_recv;
592
593     my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
594     $li->state('received');
595     return update_lineitem($mgr, $li);
596 }
597
598 sub receive_lineitem {
599     my($mgr, $li_id, $skip_complete_check) = @_;
600     my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
601
602     return 0 unless $li->state eq 'on-order' or $li->state eq 'cancelled'; # sic
603
604     $li->clear_cancel_reason; # un-cancel on receive
605
606     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
607         {lineitem => $li_id, recv_time => undef}, {idlist => 1});
608
609     for my $lid_id (@$lid_ids) {
610        receive_lineitem_detail($mgr, $lid_id, 1) or return 0; 
611     }
612
613     $mgr->add_li;
614     $li->state('received');
615
616     $li = update_lineitem($mgr, $li) or return 0;
617     $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
618
619     my $po;
620     return 0 unless
621         $skip_complete_check or (
622             $po = check_purchase_order_received($mgr, $li->purchase_order)
623         );
624
625     my $result = {"li" => {$li->id => {"state" => $li->state}}};
626     $result->{"po"} = describe_affected_po($mgr->editor, $po) if ref $po;
627     return $result;
628 }
629
630 sub rollback_receive_lineitem {
631     my($mgr, $li_id) = @_;
632     my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
633
634     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
635         {lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
636
637     for my $lid_id (@$lid_ids) {
638        rollback_receive_lineitem_detail($mgr, $lid_id, 1) or return 0; 
639     }
640
641     $mgr->add_li;
642     $li->state('on-order');
643     return update_lineitem($mgr, $li);
644 }
645
646
647 sub create_lineitem_status_events {
648     my($mgr, $li_id, $hook) = @_;
649
650     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
651     $ses->connect;
652     my $user_reqs = $mgr->editor->search_acq_user_request([
653         {lineitem => $li_id}, 
654         {flesh => 1, flesh_fields => {aur => ['usr']}}
655     ]);
656
657     for my $user_req (@$user_reqs) {
658         my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $user_req->usr->home_ou);
659         $req->recv; 
660     }
661
662     $ses->disconnect;
663     return undef;
664 }
665
666 # ----------------------------------------------------------------------------
667 # Lineitem Detail
668 # ----------------------------------------------------------------------------
669 sub create_lineitem_detail {
670     my($mgr, %args) = @_;
671     my $lid = Fieldmapper::acq::lineitem_detail->new;
672     $lid->$_($args{$_}) for keys %args;
673     $lid->clear_id;
674     $mgr->add_lid;
675     return $mgr->editor->create_acq_lineitem_detail($lid);
676 }
677
678
679 # flesh out any required data with default values where appropriate
680 sub complete_lineitem_detail {
681     my($mgr, $lid) = @_;
682     unless($lid->barcode) {
683         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
684         $lid->barcode($pfx.$lid->id);
685     }
686
687     unless($lid->cn_label) {
688         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
689         $lid->cn_label($pfx.$lid->id);
690     }
691
692     if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
693         $lid->location($loc);
694     }
695
696     $lid->circ_modifier(get_default_circ_modifier($mgr, $lid->owning_lib))
697         unless defined $lid->circ_modifier;
698
699     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
700     return $lid;
701 }
702
703 sub get_default_circ_modifier {
704     my($mgr, $org) = @_;
705     my $code = $mgr->cache($org, 'def_circ_mod');
706     $code = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier') unless defined $code;
707     return $mgr->cache($org, 'def_circ_mod', $code) if defined $code;
708     return undef;
709 }
710
711 sub delete_lineitem_detail {
712     my($mgr, $lid) = @_;
713     $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
714     return $mgr->editor->delete_acq_lineitem_detail($lid);
715 }
716
717
718 sub receive_lineitem_detail {
719     my($mgr, $lid_id, $skip_complete_check) = @_;
720     my $e = $mgr->editor;
721
722     my $lid = $e->retrieve_acq_lineitem_detail([
723         $lid_id,
724         {   flesh => 1,
725             flesh_fields => {
726                 acqlid => ['fund_debit']
727             }
728         }
729     ]) or return 0;
730
731     return 1 if $lid->recv_time;
732
733     # if the LID is marked as canceled, remove the cancel reason,
734     # and reinstate fund debits where deleted by cancelation.
735     if ($lid->cancel_reason) {
736         my $cr = $e->retrieve_acq_cancel_reason($lid->cancel_reason);
737
738         if (!$U->is_true($cr->keep_debits)) {
739             # debits were removed during cancelation.
740             create_lineitem_detail_debit(
741                 $mgr, $lid->lineitem, $lid) or return 0;
742         }
743         $lid->clear_cancel_reason;
744     }
745
746     $lid->receiver($e->requestor->id);
747     $lid->recv_time('now');
748     $e->update_acq_lineitem_detail($lid) or return 0;
749
750     if ($lid->eg_copy_id) {
751         my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
752         # only update status if it hasn't already been updated
753         $copy->status(OILS_COPY_STATUS_IN_PROCESS) if $copy->status == OILS_COPY_STATUS_ON_ORDER;
754         $copy->edit_date('now');
755         $copy->editor($e->requestor->id);
756         $copy->creator($e->requestor->id) if $U->ou_ancestor_setting_value(
757             $e->requestor->ws_ou, 'acq.copy_creator_uses_receiver', $e);
758         $e->update_asset_copy($copy) or return 0;
759     }
760
761     $mgr->add_lid;
762
763     return 1 if $skip_complete_check;
764
765     my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
766     return 1 if $li == 1; # li not received
767
768     return check_purchase_order_received($mgr, $li->purchase_order) or return 0;
769 }
770
771
772 sub rollback_receive_lineitem_detail {
773     my($mgr, $lid_id) = @_;
774     my $e = $mgr->editor;
775
776     my $lid = $e->retrieve_acq_lineitem_detail([
777         $lid_id,
778         {   flesh => 1,
779             flesh_fields => {
780                 acqlid => ['fund_debit']
781             }
782         }
783     ]) or return 0;
784
785     return 1 unless $lid->recv_time;
786
787     $lid->clear_receiver;
788     $lid->clear_recv_time;
789     $e->update_acq_lineitem_detail($lid) or return 0;
790
791     if ($lid->eg_copy_id) {
792         my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
793         $copy->status(OILS_COPY_STATUS_ON_ORDER);
794         $copy->edit_date('now');
795         $copy->editor($e->requestor->id);
796         $e->update_asset_copy($copy) or return 0;
797     }
798
799     $mgr->add_lid;
800     return $lid;
801 }
802
803 # ----------------------------------------------------------------------------
804 # Lineitem Attr
805 # ----------------------------------------------------------------------------
806 sub set_lineitem_attr {
807     my($mgr, %args) = @_;
808     my $attr_type = $args{attr_type};
809
810     # first, see if it's already set.  May just need to overwrite it
811     my $attr = $mgr->editor->search_acq_lineitem_attr({
812         lineitem => $args{lineitem},
813         attr_type => $args{attr_type},
814         attr_name => $args{attr_name}
815     })->[0];
816
817     if($attr) {
818         $attr->attr_value($args{attr_value});
819         return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
820         return undef;
821
822     } else {
823
824         $attr = Fieldmapper::acq::lineitem_attr->new;
825         $attr->$_($args{$_}) for keys %args;
826         
827         unless($attr->definition) {
828             my $find = "search_acq_$attr_type";
829             my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
830             $attr->definition($attr_def_id);
831         }
832         return $mgr->editor->create_acq_lineitem_attr($attr);
833     }
834 }
835
836 # ----------------------------------------------------------------------------
837 # Lineitem Debits
838 # ----------------------------------------------------------------------------
839 sub create_lineitem_debits {
840     my ($mgr, $li, $options) = @_;
841     $options ||= {};
842     my $dry_run = $options->{dry_run};
843
844     unless($li->estimated_unit_price) {
845         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
846         $mgr->editor->rollback;
847         return 0;
848     }
849
850     unless($li->provider) {
851         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
852         $mgr->editor->rollback;
853         return 0;
854     }
855
856     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
857         {lineitem => $li->id}, 
858         {idlist=>1}
859     );
860
861     if (@$lid_ids == 0 and !$options->{zero_copy_activate}) {
862         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_COPIES', payload => $li->id));
863         $mgr->editor->rollback;
864         return 0;
865     }
866
867     for my $lid_id (@$lid_ids) {
868
869         my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
870             $lid_id,
871             {   flesh => 1, 
872                 flesh_fields => {acqlid => ['fund']}
873             }
874         ]);
875
876         create_lineitem_detail_debit($mgr, $li, $lid, $dry_run) or return 0;
877     }
878
879     return 1;
880 }
881
882
883 # flesh li->provider
884 # flesh lid->fund
885 sub create_lineitem_detail_debit {
886     my ($mgr, $li, $lid, $dry_run, $no_translate) = @_;
887
888     # don't create the debit if one already exists
889     return $mgr->editor->retrieve_acq_fund_debit($lid->fund_debit) if $lid->fund_debit;
890
891     my $li_id = ref($li) ? $li->id : $li;
892
893     unless(ref $li and ref $li->provider) {
894        $li = $mgr->editor->retrieve_acq_lineitem([
895             $li_id,
896             {   flesh => 1,
897                 flesh_fields => {jub => ['provider']},
898             }
899         ]);
900     }
901
902     if(ref $lid) {
903         $lid->fund($mgr->editor->retrieve_acq_fund($lid->fund)) unless(ref $lid->fund);
904     } else {
905         $lid = $mgr->editor->retrieve_acq_lineitem_detail([
906             $lid,
907             {   flesh => 1, 
908                 flesh_fields => {acqlid => ['fund']}
909             }
910         ]);
911     }
912
913     unless ($lid->fund) {
914         $mgr->editor->event(
915             new OpenILS::Event("ACQ_FUND_NOT_FOUND") # close enough
916         );
917         return 0;
918     }
919
920     my $amount = $li->estimated_unit_price;
921     if($li->provider->currency_type ne $lid->fund->currency_type and !$no_translate) {
922
923         # At Fund debit creation time, translate into the currency of the fund
924         # TODO: org setting to disable automatic currency conversion at debit create time?
925
926         $amount = $mgr->editor->json_query({
927             from => [
928                 'acq.exchange_ratio', 
929                 $li->provider->currency_type, # source currency
930                 $lid->fund->currency_type, # destination currency
931                 $li->estimated_unit_price # source amount
932             ]
933         })->[0]->{'acq.exchange_ratio'};
934     }
935
936     my $debit = create_fund_debit(
937         $mgr, 
938         $dry_run,
939         fund => $lid->fund->id,
940         origin_amount => $li->estimated_unit_price,
941         origin_currency_type => $li->provider->currency_type,
942         amount => $amount
943     ) or return 0;
944
945     $lid->fund_debit($debit->id);
946     $lid->fund($lid->fund->id);
947     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
948     return $debit;
949 }
950
951
952 __PACKAGE__->register_method(
953     "method" => "fund_exceeds_balance_percent_api",
954     "api_name" => "open-ils.acq.fund.check_balance_percentages",
955     "signature" => {
956         "desc" => q/Determine whether a given fund exceeds its defined
957             "balance stop and warning percentages"/,
958         "params" => [
959             {"desc" => "Authentication token", "type" => "string"},
960             {"desc" => "Fund ID", "type" => "number"},
961             {"desc" => "Theoretical debit amount (optional)",
962                 "type" => "number"}
963         ],
964         "return" => {"desc" => q/An array of two values, for stop and warning,
965             in that order: 1 if fund exceeds that balance percentage, else 0/}
966     }
967 );
968
969 sub fund_exceeds_balance_percent_api {
970     my ($self, $conn, $auth, $fund_id, $debit_amount) = @_;
971
972     $debit_amount ||= 0;
973
974     my $e = new_editor("authtoken" => $auth);
975     return $e->die_event unless $e->checkauth;
976
977     my $fund = $e->retrieve_acq_fund($fund_id) or return $e->die_event;
978     return $e->die_event unless $e->allowed("VIEW_FUND", $fund->org);
979
980     my $result = [
981         fund_exceeds_balance_percent($fund, $debit_amount, $e, "stop"),
982         fund_exceeds_balance_percent($fund, $debit_amount, $e, "warning")
983     ];
984
985     $e->disconnect;
986     return $result;
987 }
988
989 sub fund_exceeds_balance_percent {
990     my ($fund, $debit_amount, $e, $which) = @_;
991
992     my ($method_name, $event_name) = @{{
993         "warning" => [
994             "balance_warning_percent", "ACQ_FUND_EXCEEDS_WARN_PERCENT"
995         ],
996         "stop" => [
997             "balance_stop_percent", "ACQ_FUND_EXCEEDS_STOP_PERCENT"
998         ]
999     }->{$which}};
1000
1001     if ($fund->$method_name) {
1002         my $balance =
1003             $e->search_acq_fund_combined_balance({"fund" => $fund->id})->[0];
1004         my $allocations =
1005             $e->search_acq_fund_allocation_total({"fund" => $fund->id})->[0];
1006
1007         $balance = ($balance) ? $balance->amount : 0;
1008         $allocations = ($allocations) ? $allocations->amount : 0;
1009
1010         if ( 
1011             $allocations == 0 || # if no allocations were ever made, assume we have hit the stop percent
1012             ((($allocations - $balance + $debit_amount) / $allocations) * 100) > $fund->$method_name
1013         ) {
1014             $logger->info("fund would hit a limit: " . $fund->id . ", $balance, $debit_amount, $allocations, $method_name");
1015             $e->event(
1016                 new OpenILS::Event(
1017                     $event_name,
1018                     "payload" => {
1019                         "fund" => $fund, "debit_amount" => $debit_amount
1020                     }
1021                 )
1022             );
1023             return 1;
1024         }
1025     }
1026     return 0;
1027 }
1028
1029 # ----------------------------------------------------------------------------
1030 # Fund Debit
1031 # ----------------------------------------------------------------------------
1032 sub create_fund_debit {
1033     my($mgr, $dry_run, %args) = @_;
1034
1035     # Verify the fund is not being spent beyond the hard stop amount
1036     my $fund = $mgr->editor->retrieve_acq_fund($args{fund}) or return 0;
1037
1038     return 0 if
1039         fund_exceeds_balance_percent(
1040             $fund, $args{"amount"}, $mgr->editor, "stop"
1041         );
1042     return 0 if
1043         $dry_run and fund_exceeds_balance_percent(
1044             $fund, $args{"amount"}, $mgr->editor, "warning"
1045         );
1046
1047     my $debit = Fieldmapper::acq::fund_debit->new;
1048     $debit->debit_type('purchase');
1049     $debit->encumbrance('t');
1050     $debit->$_($args{$_}) for keys %args;
1051     $debit->clear_id;
1052     $mgr->add_debit($debit->amount);
1053     return $mgr->editor->create_acq_fund_debit($debit);
1054 }
1055
1056
1057 # ----------------------------------------------------------------------------
1058 # Picklist
1059 # ----------------------------------------------------------------------------
1060 sub create_picklist {
1061     my($mgr, %args) = @_;
1062     my $picklist = Fieldmapper::acq::picklist->new;
1063     $picklist->creator($mgr->editor->requestor->id);
1064     $picklist->owner($picklist->creator);
1065     $picklist->editor($picklist->creator);
1066     $picklist->create_time('now');
1067     $picklist->edit_time('now');
1068     $picklist->org_unit($mgr->editor->requestor->ws_ou);
1069     $picklist->owner($mgr->editor->requestor->id);
1070     $picklist->$_($args{$_}) for keys %args;
1071     $picklist->clear_id;
1072     $mgr->picklist($picklist);
1073     return $mgr->editor->create_acq_picklist($picklist);
1074 }
1075
1076 sub update_picklist {
1077     my($mgr, $picklist) = @_;
1078     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1079     $picklist->edit_time('now');
1080     $picklist->editor($mgr->editor->requestor->id);
1081     if ($mgr->editor->update_acq_picklist($picklist)) {
1082         $picklist = $mgr->editor->retrieve_acq_picklist($mgr->editor->data);
1083         $mgr->picklist($picklist);
1084         return $picklist;
1085     } else {
1086         return undef;
1087     }
1088 }
1089
1090 sub delete_picklist {
1091     my($mgr, $picklist) = @_;
1092     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1093
1094     # delete all 'new' lineitems
1095     my $li_ids = $mgr->editor->search_acq_lineitem(
1096         {
1097             picklist => $picklist->id,
1098             "-or" => {state => "new", purchase_order => undef}
1099         },
1100         {idlist => 1}
1101     );
1102     for my $li_id (@$li_ids) {
1103         my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1104         return 0 unless delete_lineitem($mgr, $li);
1105         $mgr->respond;
1106     }
1107
1108     # detach all non-'new' lineitems
1109     $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}}, {idlist => 1});
1110     for my $li_id (@$li_ids) {
1111         my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1112         $li->clear_picklist;
1113         return 0 unless update_lineitem($mgr, $li);
1114         $mgr->respond;
1115     }
1116
1117     # remove any picklist-specific object perms
1118     my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
1119     for my $op (@$ops) {
1120         return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
1121     }
1122
1123     return $mgr->editor->delete_acq_picklist($picklist);
1124 }
1125
1126 # ----------------------------------------------------------------------------
1127 # Purchase Order
1128 # ----------------------------------------------------------------------------
1129 sub update_purchase_order {
1130     my($mgr, $po) = @_;
1131     $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
1132     $po->editor($mgr->editor->requestor->id);
1133     $po->edit_time('now');
1134     $mgr->purchase_order($po);
1135     return $mgr->editor->retrieve_acq_purchase_order($mgr->editor->data)
1136         if $mgr->editor->update_acq_purchase_order($po);
1137     return undef;
1138 }
1139
1140 sub create_purchase_order {
1141     my($mgr, %args) = @_;
1142
1143     # verify the chosen provider is still active
1144     my $provider = $mgr->editor->retrieve_acq_provider($args{provider}) or return 0;
1145     unless($U->is_true($provider->active)) {
1146         $logger->error("provider is not active.  cannot create PO");
1147         $mgr->editor->event(OpenILS::Event->new('ACQ_PROVIDER_INACTIVE'));
1148         return 0;
1149     }
1150
1151     my $po = Fieldmapper::acq::purchase_order->new;
1152     $po->creator($mgr->editor->requestor->id);
1153     $po->editor($mgr->editor->requestor->id);
1154     $po->owner($mgr->editor->requestor->id);
1155     $po->edit_time('now');
1156     $po->create_time('now');
1157     $po->state('pending');
1158     $po->ordering_agency($mgr->editor->requestor->ws_ou);
1159     $po->$_($args{$_}) for keys %args;
1160     $po->clear_id;
1161     $mgr->purchase_order($po);
1162     return $mgr->editor->create_acq_purchase_order($po);
1163 }
1164
1165 # ----------------------------------------------------------------------------
1166 # if all of the lineitems for this PO are received,
1167 # mark the PO as received
1168 # ----------------------------------------------------------------------------
1169 sub check_purchase_order_received {
1170     my($mgr, $po_id) = @_;
1171
1172     my $non_recv_li = $mgr->editor->search_acq_lineitem(
1173         {   purchase_order => $po_id,
1174             state => {'!=' => 'received'}
1175         }, {idlist=>1});
1176
1177     my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
1178     return $po if @$non_recv_li;
1179
1180     $po->state('received');
1181     return update_purchase_order($mgr, $po);
1182 }
1183
1184
1185 # ----------------------------------------------------------------------------
1186 # Bib, Callnumber, and Copy data
1187 # ----------------------------------------------------------------------------
1188
1189 sub create_lineitem_assets {
1190     my($mgr, $li_id) = @_;
1191     my $evt;
1192
1193     my $li = $mgr->editor->retrieve_acq_lineitem([
1194         $li_id,
1195         {   flesh => 1,
1196             flesh_fields => {jub => ['purchase_order', 'attributes']}
1197         }
1198     ]) or return 0;
1199
1200     # note: at this point, the bib record this LI links to should already be created
1201
1202     # -----------------------------------------------------------------
1203     # The lineitem is going live, promote user request holds to real holds
1204     # -----------------------------------------------------------------
1205     promote_lineitem_holds($mgr, $li) or return 0;
1206
1207     my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
1208
1209     # -----------------------------------------------------------------
1210     # for each lineitem_detail, create the volume if necessary, create 
1211     # a copy, and link them all together.
1212     # -----------------------------------------------------------------
1213     my $first_cn;
1214     for my $lid_id (@{$li_details}) {
1215
1216         my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
1217         next if $lid->eg_copy_id;
1218
1219         # use the same callnumber label for all items within this lineitem
1220         $lid->cn_label($first_cn) if $first_cn and not $lid->cn_label;
1221
1222         # apply defaults if necessary
1223         return 0 unless complete_lineitem_detail($mgr, $lid);
1224
1225         $first_cn = $lid->cn_label unless $first_cn;
1226
1227         my $org = $lid->owning_lib;
1228         my $label = $lid->cn_label;
1229         my $bibid = $li->eg_bib_id;
1230
1231         my $volume = $mgr->cache($org, "cn.$bibid.$label");
1232         unless($volume) {
1233             $volume = create_volume($mgr, $li, $lid) or return 0;
1234             $mgr->cache($org, "cn.$bibid.$label", $volume);
1235         }
1236         create_copy($mgr, $volume, $lid, $li) or return 0;
1237     }
1238
1239     return { li => $li };
1240 }
1241
1242 sub create_volume {
1243     my($mgr, $li, $lid) = @_;
1244
1245     my ($volume, $evt) = 
1246         OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
1247             $mgr->editor, 
1248             $lid->cn_label, 
1249             $li->eg_bib_id, 
1250             $lid->owning_lib
1251         );
1252
1253     if($evt) {
1254         $mgr->editor->event($evt);
1255         return 0;
1256     }
1257
1258     return $volume;
1259 }
1260
1261 sub create_copy {
1262     my($mgr, $volume, $lid, $li) = @_;
1263     my $copy = Fieldmapper::asset::copy->new;
1264     $copy->isnew(1);
1265     $copy->loan_duration(2);
1266     $copy->fine_level(2);
1267     $copy->status(($lid->recv_time) ? OILS_COPY_STATUS_IN_PROCESS : OILS_COPY_STATUS_ON_ORDER);
1268     $copy->barcode($lid->barcode);
1269     $copy->location($lid->location);
1270     $copy->call_number($volume->id);
1271     $copy->circ_lib($volume->owning_lib);
1272     $copy->circ_modifier($lid->circ_modifier);
1273
1274     # AKA list price.  We might need a $li->list_price field since 
1275     # estimated price is not necessarily the same as list price
1276     $copy->price($li->estimated_unit_price); 
1277
1278     my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
1279     if($evt) {
1280         $mgr->editor->event($evt);
1281         return 0;
1282     }
1283
1284     $mgr->add_copy;
1285     $lid->eg_copy_id($copy->id);
1286     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
1287 }
1288
1289
1290
1291
1292
1293
1294 # ----------------------------------------------------------------------------
1295 # Workflow: Build a selection list from a Z39.50 search
1296 # ----------------------------------------------------------------------------
1297
1298 __PACKAGE__->register_method(
1299     method => 'zsearch',
1300     api_name => 'open-ils.acq.picklist.search.z3950',
1301     stream => 1,
1302     signature => {
1303         desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
1304         params => [
1305             {desc => 'Authentication token', type => 'string'},
1306             {desc => 'Search definition', type => 'object'},
1307             {desc => 'Picklist name, optional', type => 'string'},
1308         ]
1309     }
1310 );
1311
1312 sub zsearch {
1313     my($self, $conn, $auth, $search, $name, $options) = @_;
1314     my $e = new_editor(authtoken=>$auth);
1315     return $e->event unless $e->checkauth;
1316     return $e->event unless $e->allowed('CREATE_PICKLIST');
1317
1318     $search->{limit} ||= 10;
1319     $options ||= {};
1320
1321     my $ses = OpenSRF::AppSession->create('open-ils.search');
1322     my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
1323
1324     my $first = 1;
1325     my $picklist;
1326     my $mgr;
1327     while(my $resp = $req->recv(timeout=>60)) {
1328
1329         if($first) {
1330             my $e = new_editor(requestor=>$e->requestor, xact=>1);
1331             $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1332             $picklist = zsearch_build_pl($mgr, $name);
1333             $first = 0;
1334         }
1335
1336         my $result = $resp->content;
1337         my $count = $result->{count} || 0;
1338         $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
1339
1340         for my $rec (@{$result->{records}}) {
1341
1342             my $li = create_lineitem($mgr, 
1343                 picklist => $picklist->id,
1344                 source_label => $result->{service},
1345                 marc => $rec->{marcxml},
1346                 eg_bib_id => $rec->{bibid}
1347             );
1348
1349             if($$options{respond_li}) {
1350                 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
1351                     if $$options{flesh_attrs};
1352                 $li->clear_marc if $$options{clear_marc};
1353                 $mgr->respond(lineitem => $li);
1354             } else {
1355                 $mgr->respond;
1356             }
1357         }
1358     }
1359
1360     $mgr->editor->commit;
1361     return $mgr->respond_complete;
1362 }
1363
1364 sub zsearch_build_pl {
1365     my($mgr, $name) = @_;
1366     $name ||= '';
1367
1368     my $picklist = $mgr->editor->search_acq_picklist({
1369         owner => $mgr->editor->requestor->id, 
1370         name => $name
1371     })->[0];
1372
1373     if($name eq '' and $picklist) {
1374         return 0 unless delete_picklist($mgr, $picklist);
1375         $picklist = undef;
1376     }
1377
1378     return update_picklist($mgr, $picklist) if $picklist;
1379     return create_picklist($mgr, name => $name);
1380 }
1381
1382
1383 # ----------------------------------------------------------------------------
1384 # Workflow: Build a selection list / PO by importing a batch of MARC records
1385 # ----------------------------------------------------------------------------
1386
1387 __PACKAGE__->register_method(
1388     method   => 'upload_records',
1389     api_name => 'open-ils.acq.process_upload_records',
1390     stream   => 1,
1391     max_chunk_count => 1
1392 );
1393
1394 sub upload_records {
1395     my($self, $conn, $auth, $key, $args) = @_;
1396     $args ||= {};
1397
1398     my $e = new_editor(authtoken => $auth, xact => 1);
1399     return $e->die_event unless $e->checkauth;
1400     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1401
1402     my $cache = OpenSRF::Utils::Cache->new;
1403
1404     my $data = $cache->get_cache("vandelay_import_spool_$key");
1405     my $filename        = $data->{path};
1406     my $provider        = $args->{provider};
1407     my $picklist        = $args->{picklist};
1408     my $create_po       = $args->{create_po};
1409     my $activate_po     = $args->{activate_po};
1410     my $vandelay        = $args->{vandelay};
1411     my $ordering_agency = $args->{ordering_agency} || $e->requestor->ws_ou;
1412     my $fiscal_year     = $args->{fiscal_year};
1413
1414     # if the user provides no fiscal year, find the
1415     # current fiscal year for the ordering agency.
1416     $fiscal_year ||= $U->simplereq(
1417         'open-ils.acq',
1418         'open-ils.acq.org_unit.current_fiscal_year',
1419         $auth,
1420         $ordering_agency
1421     );
1422
1423     my $po;
1424     my $evt;
1425
1426     unless(-r $filename) {
1427         $logger->error("unable to read MARC file $filename");
1428         $e->rollback;
1429         return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
1430     }
1431
1432     $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
1433
1434     if($picklist) {
1435         $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
1436         if($picklist->owner != $e->requestor->id) {
1437             return $e->die_event unless 
1438                 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
1439         }
1440         $mgr->picklist($picklist);
1441     }
1442
1443     if($create_po) {
1444         return $e->die_event unless 
1445             $e->allowed('CREATE_PURCHASE_ORDER', $ordering_agency);
1446
1447         $po = create_purchase_order($mgr, 
1448             ordering_agency => $ordering_agency,
1449             provider => $provider->id,
1450             state => 'pending' # will be updated later if activated
1451         ) or return $mgr->editor->die_event;
1452     }
1453
1454     $logger->info("acq processing MARC file=$filename");
1455
1456     my $batch = new MARC::Batch ('USMARC', $filename);
1457     $batch->strict_off;
1458
1459     my $count = 0;
1460     my @li_list;
1461
1462     while(1) {
1463
1464         my ($err, $xml, $r);
1465         $count++;
1466
1467         try {
1468             $r = $batch->next;
1469         } catch Error with {
1470             $err = shift;
1471             $logger->warn("Proccessing of record $count in set $key failed with error $err.  Skipping this record");
1472         };
1473
1474         next if $err;
1475         last unless $r;
1476
1477         try {
1478             $xml = clean_marc($r);
1479         } catch Error with {
1480             $err = shift;
1481             $logger->warn("Proccessing XML of record $count in set $key failed with error $err.  Skipping this record");
1482         };
1483
1484         next if $err or not $xml;
1485
1486         my %args = (
1487             source_label => $provider->code,
1488             provider => $provider->id,
1489             marc => $xml,
1490         );
1491
1492         $args{picklist} = $picklist->id if $picklist;
1493         if($po) {
1494             $args{purchase_order} = $po->id;
1495             $args{state} = 'pending-order';
1496         }
1497
1498         my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
1499         $mgr->respond;
1500         $li->provider($provider); # flesh it, we'll need it later
1501
1502         import_lineitem_details($mgr, $ordering_agency, $li, $fiscal_year) 
1503             or return $mgr->editor->die_event;
1504         $mgr->respond;
1505
1506         push(@li_list, $li->id);
1507         $mgr->respond;
1508     }
1509
1510     if ($po) {
1511         $evt = extract_po_name($mgr, $po, \@li_list);
1512         return $evt if $evt;
1513     }
1514
1515     $e->commit;
1516     unlink($filename);
1517     $cache->delete_cache('vandelay_import_spool_' . $key);
1518
1519     if ($po and $activate_po) {
1520         my $die_event = activate_purchase_order_impl($mgr, $po->id, $vandelay);
1521         return $die_event if $die_event;
1522
1523     } elsif ($vandelay) {
1524         $vandelay->{new_rec_perm} = 'IMPORT_ACQ_LINEITEM_BIB_RECORD_UPLOAD';
1525         create_lineitem_list_assets($mgr, \@li_list, $vandelay, 
1526             !$vandelay->{create_assets}) or return $e->die_event;
1527     }
1528
1529     return $mgr->respond_complete;
1530 }
1531
1532 # see if the PO name is encoded in the newly imported records
1533 sub extract_po_name {
1534     my ($mgr, $po, $li_ids) = @_;
1535     my $e = $mgr->editor;
1536
1537     # find the first instance of the name
1538     my $attr = $e->search_acq_lineitem_attr([
1539         {   lineitem => $li_ids,
1540             attr_type => 'lineitem_provider_attr_definition',
1541             attr_name => 'purchase_order'
1542         }, {
1543             order_by => {aqlia => 'id'},
1544             limit => 1
1545         }
1546     ])->[0] or return undef;
1547
1548     my $name = $attr->attr_value;
1549
1550     # see if another PO already has the name, provider, and org
1551     my $existing = $e->search_acq_purchase_order(
1552         {   name => $name,
1553             ordering_agency => $po->ordering_agency,
1554             provider => $po->provider
1555         },
1556         {idlist => 1}
1557     )->[0];
1558
1559     # if a PO exists with the same name (and provider/org)
1560     # tack the po ID into the name to differentiate
1561     $name = sprintf("$name (%s)", $po->id) if $existing;
1562
1563     $logger->info("Extracted PO name: $name");
1564
1565     $po->name($name);
1566     update_purchase_order($mgr, $po) or return $e->die_event;
1567     return undef;
1568 }
1569
1570 sub import_lineitem_details {
1571     my($mgr, $ordering_agency, $li, $fiscal_year) = @_;
1572
1573     my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
1574     return 1 unless @$holdings;
1575     my $org_path = $U->get_org_ancestors($ordering_agency);
1576     $org_path = [ reverse (@$org_path) ];
1577     my $price;
1578
1579
1580     my $idx = 1;
1581     while(1) {
1582         # create a lineitem detail for each copy in the data
1583
1584         my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx, $fiscal_year);
1585         last unless defined $compiled;
1586         return 0 unless $compiled;
1587
1588         # this takes the price of the last copy and uses it as the lineitem price
1589         # need to determine if a given record would include different prices for the same item
1590         $price = $$compiled{estimated_price};
1591
1592         last unless $$compiled{quantity};
1593
1594         for(1..$$compiled{quantity}) {
1595             my $lid = create_lineitem_detail(
1596                 $mgr, 
1597                 lineitem        => $li->id,
1598                 owning_lib      => $$compiled{owning_lib},
1599                 cn_label        => $$compiled{call_number},
1600                 fund            => $$compiled{fund},
1601                 circ_modifier   => $$compiled{circ_modifier},
1602                 note            => $$compiled{note},
1603                 location        => $$compiled{copy_location},
1604                 collection_code => $$compiled{collection_code},
1605                 barcode         => $$compiled{barcode}
1606             ) or return 0;
1607         }
1608
1609         $mgr->respond;
1610         $idx++;
1611     }
1612
1613     $li->estimated_unit_price($price);
1614     update_lineitem($mgr, $li) or return 0;
1615     return 1;
1616 }
1617
1618 # return hash on success, 0 on error, undef on no more holdings
1619 sub extract_lineitem_detail_data {
1620     my($mgr, $org_path, $holdings, $index, $fiscal_year) = @_;
1621
1622     my @data_list = grep { $_->{holding} eq $index } @$holdings;
1623     return undef unless @data_list;
1624
1625     my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1626     my $base_org = $$org_path[0];
1627
1628     my $killme = sub {
1629         my $msg = shift;
1630         $logger->error("Item import extraction error: $msg");
1631         $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1632         $mgr->editor->rollback;
1633         $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1634         return 0;
1635     };
1636
1637     # ---------------------------------------------------------------------
1638     # Fund
1639     if(my $code = $compiled{fund_code}) {
1640
1641         my $fund = $mgr->cache($base_org, "fund.$code");
1642         unless($fund) {
1643             # search up the org tree for the most appropriate fund
1644             for my $org (@$org_path) {
1645                 $fund = $mgr->editor->search_acq_fund(
1646                     {org => $org, code => $code, year => $fiscal_year}, {idlist => 1})->[0];
1647                 last if $fund;
1648             }
1649         }
1650         return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1651         $compiled{fund} = $fund;
1652         $mgr->cache($base_org, "fund.$code", $fund);
1653     }
1654
1655
1656     # ---------------------------------------------------------------------
1657     # Owning lib
1658     if(my $sn = $compiled{owning_lib}) {
1659         my $org_id = $mgr->cache($base_org, "orgsn.$sn") ||
1660             $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1661         return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1662         $compiled{owning_lib} = $org_id;
1663         $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1664     }
1665
1666
1667     # ---------------------------------------------------------------------
1668     # Circ Modifier
1669     my $code = $compiled{circ_modifier};
1670
1671     if(defined $code) {
1672
1673         # verify this is a valid circ modifier
1674         return $killme->("invlalid circ_modifier $code") unless 
1675             defined $mgr->cache($base_org, "mod.$code") or 
1676             $mgr->editor->retrieve_config_circ_modifier($code);
1677
1678             # if valid, cache for future tests
1679             $mgr->cache($base_org, "mod.$code", $code);
1680
1681     } else {
1682         $compiled{circ_modifier} = get_default_circ_modifier($mgr, $base_org);
1683     }
1684
1685
1686     # ---------------------------------------------------------------------
1687     # Shelving Location
1688     if( my $name = $compiled{copy_location}) {
1689
1690         my $cp_base_org = $base_org;
1691
1692         if ($compiled{owning_lib}) {
1693             # start looking for copy locations at the copy 
1694             # owning lib instaed of the upload context org
1695             $cp_base_org = $compiled{owning_lib};
1696         }
1697
1698         my $loc = $mgr->cache($cp_base_org, "copy_loc.$name");
1699         unless($loc) {
1700             my $org = $cp_base_org;
1701             while ($org) {
1702                 $loc = $mgr->editor->search_asset_copy_location(
1703                     {owning_lib => $org, name => $name}, {idlist => 1})->[0];
1704                 last if $loc;
1705                 $org = $mgr->editor->retrieve_actor_org_unit($org)->parent_ou;
1706             }
1707         }
1708         return $killme->("Invalid copy location $name") unless $loc;
1709         $compiled{copy_location} = $loc;
1710         $mgr->cache($cp_base_org, "copy_loc.$name", $loc);
1711     }
1712
1713     return \%compiled;
1714 }
1715
1716
1717
1718 # ----------------------------------------------------------------------------
1719 # Workflow: Given an existing purchase order, import/create the bibs, 
1720 # callnumber and copy objects
1721 # ----------------------------------------------------------------------------
1722
1723 __PACKAGE__->register_method(
1724     method => 'create_po_assets',
1725     api_name    => 'open-ils.acq.purchase_order.assets.create',
1726     signature => {
1727         desc => q/Creates assets for each lineitem in the purchase order/,
1728         params => [
1729             {desc => 'Authentication token', type => 'string'},
1730             {desc => 'The purchase order id', type => 'number'},
1731         ],
1732         return => {desc => 'Streams a total versus completed counts object, event on error'}
1733     },
1734     max_chunk_count => 1
1735 );
1736
1737 sub create_po_assets {
1738     my($self, $conn, $auth, $po_id, $args) = @_;
1739     $args ||= {};
1740
1741     my $e = new_editor(authtoken=>$auth, xact=>1);
1742     return $e->die_event unless $e->checkauth;
1743     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1744
1745     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1746
1747     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1748
1749     # it's ugly, but it's fast.  Get the total count of lineitem detail objects to process
1750     my $lid_total = $e->json_query({
1751         select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] }, 
1752         from => {
1753             acqlid => {
1754                 jub => {
1755                     fkey => 'lineitem', 
1756                     field => 'id', 
1757                     join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1758                 }
1759             }
1760         }, 
1761         where => {'+acqpo' => {id => $po_id}}
1762     })->[0]->{id};
1763
1764     $mgr->total(scalar(@$li_ids) + $lid_total);
1765
1766     create_lineitem_list_assets($mgr, $li_ids, $args->{vandelay}) 
1767         or return $e->die_event;
1768
1769     $e->xact_begin;
1770     update_purchase_order($mgr, $po) or return $e->die_event;
1771     $e->commit;
1772
1773     return $mgr->respond_complete;
1774 }
1775
1776
1777
1778 __PACKAGE__->register_method(
1779     method    => 'create_purchase_order_api',
1780     api_name  => 'open-ils.acq.purchase_order.create',
1781     signature => {
1782         desc   => 'Creates a new purchase order',
1783         params => [
1784             {desc => 'Authentication token', type => 'string'},
1785             {desc => 'purchase_order to create', type => 'object'}
1786         ],
1787         return => {desc => 'The purchase order id, Event on failure'}
1788     },
1789     max_chunk_count => 1
1790 );
1791
1792 sub create_purchase_order_api {
1793     my($self, $conn, $auth, $po, $args) = @_;
1794     $args ||= {};
1795
1796     my $e = new_editor(xact=>1, authtoken=>$auth);
1797     return $e->die_event unless $e->checkauth;
1798     return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1799     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1800
1801     # create the PO
1802     my %pargs = (ordering_agency => $e->requestor->ws_ou); # default
1803     $pargs{provider}            = $po->provider            if $po->provider;
1804     $pargs{ordering_agency}     = $po->ordering_agency     if $po->ordering_agency;
1805     $pargs{prepayment_required} = $po->prepayment_required if $po->prepayment_required;
1806     my $vandelay = $args->{vandelay};
1807         
1808     $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1809
1810     my $li_ids = $$args{lineitems};
1811
1812     if($li_ids) {
1813
1814         for my $li_id (@$li_ids) { 
1815
1816             my $li = $e->retrieve_acq_lineitem([
1817                 $li_id,
1818                 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1819             ]) or return $e->die_event;
1820
1821             return $e->die_event(
1822                 new OpenILS::Event(
1823                     "BAD_PARAMS", payload => $li,
1824                         note => "acq.lineitem #" . $li->id .
1825                         ": purchase_order #" . $li->purchase_order
1826                 )
1827             ) if $li->purchase_order;
1828
1829             $li->provider($po->provider);
1830             $li->purchase_order($po->id);
1831             $li->state('pending-order');
1832             update_lineitem($mgr, $li) or return $e->die_event;
1833             $mgr->respond;
1834         }
1835     }
1836
1837     # see if we have a PO name encoded in any of our lineitems
1838     my $evt = extract_po_name($mgr, $po, $li_ids);
1839     return $evt if $evt;
1840
1841     # commit before starting the asset creation
1842     $e->xact_commit;
1843
1844     if($li_ids) {
1845
1846         if ($vandelay) {
1847             create_lineitem_list_assets(
1848                 $mgr, $li_ids, $vandelay, !$$args{create_assets}) 
1849                 or return $e->die_event;
1850         }
1851
1852         $e->xact_begin;
1853         apply_default_copies($mgr, $po) or return $e->die_event;
1854         $e->xact_commit;
1855     }
1856
1857     return $mgr->respond_complete;
1858 }
1859
1860 # !transaction must be managed by the caller
1861 # creates the default number of copies for each lineitem on the PO.
1862 # when a LI already has copies attached, no default copies are added.
1863 # without li_id, all lineitems are checked/applied
1864 # returns 1 on success, 0 on error
1865 sub apply_default_copies {
1866     my ($mgr, $po, $li_id) = @_;
1867
1868     my $e = $mgr->editor;
1869
1870     my $provider = ref($po->provider) ? $po->provider :
1871         $e->retrieve_acq_provider($po->provider);
1872
1873     my $copy_count = $provider->default_copy_count || return 1;
1874     
1875     $logger->info("Applying $copy_count default copies for PO ".$po->id);
1876
1877     my $li_ids = $li_id ? [$li_id] : 
1878         $e->search_acq_lineitem({
1879             purchase_order => $po->id,
1880             cancel_reason => undef
1881         }, 
1882         {idlist => 1}
1883     );
1884     
1885     for my $li_id (@$li_ids) {
1886
1887         my $lid_ids = $e->search_acq_lineitem_detail(
1888             {lineitem => $li_id}, {idlist => 1});
1889
1890         # do not apply default copies when copies already exist
1891         next if @$lid_ids;
1892
1893         for (1 .. $copy_count) {
1894             create_lineitem_detail($mgr, 
1895                 lineitem => $li_id,
1896                 owning_lib => $e->requestor->ws_ou
1897             ) or return 0;
1898         }
1899     }
1900
1901     return 1;
1902 }
1903
1904
1905
1906 __PACKAGE__->register_method(
1907     method   => 'update_lineitem_fund_batch',
1908     api_name => 'open-ils.acq.lineitem.fund.update.batch',
1909     stream   => 1,
1910     signature => { 
1911         desc => q/Given a set of lineitem IDS, updates the fund for all attached lineitem details/
1912     }
1913 );
1914
1915 sub update_lineitem_fund_batch {
1916     my($self, $conn, $auth, $li_ids, $fund_id) = @_;
1917     my $e = new_editor(xact=>1, authtoken=>$auth);
1918     return $e->die_event unless $e->checkauth;
1919     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1920     for my $li_id (@$li_ids) {
1921         my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
1922         return $evt if $evt;
1923         my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id});
1924         $_->fund($fund_id) and $_->ischanged(1) for @$li_details;
1925         $evt = lineitem_detail_CUD_batch($mgr, $li_details);
1926         return $evt if $evt;
1927         $mgr->add_li;
1928         $mgr->respond;
1929     }
1930     $e->commit;
1931     return $mgr->respond_complete;
1932 }
1933
1934
1935
1936 __PACKAGE__->register_method(
1937     method    => 'lineitem_detail_CUD_batch_api',
1938     api_name  => 'open-ils.acq.lineitem_detail.cud.batch',
1939     stream    => 1,
1940     signature => {
1941         desc   => q/Creates a new purchase order line item detail. / .
1942                   q/Additionally creates the associated fund_debit/,
1943         params => [
1944             {desc => 'Authentication token', type => 'string'},
1945             {desc => 'List of lineitem_details to create', type => 'array'},
1946             {desc => 'Create Debits.  Used for creating post-po-asset-creation debits', type => 'bool'},
1947         ],
1948         return => {desc => 'Streaming response of current position in the array'}
1949     }
1950 );
1951
1952 __PACKAGE__->register_method(
1953     method    => 'lineitem_detail_CUD_batch_api',
1954     api_name  => 'open-ils.acq.lineitem_detail.cud.batch.dry_run',
1955     stream    => 1,
1956     signature => { 
1957         desc => q/
1958             Dry run version of open-ils.acq.lineitem_detail.cud.batch.
1959             In dry_run mode, updated fund_debit's the exceed the warning
1960             percent return an event.  
1961         /
1962     }
1963 );
1964
1965
1966 sub lineitem_detail_CUD_batch_api {
1967     my($self, $conn, $auth, $li_details, $create_debits) = @_;
1968     my $e = new_editor(xact=>1, authtoken=>$auth);
1969     return $e->die_event unless $e->checkauth;
1970     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1971     my $dry_run = ($self->api_name =~ /dry_run/o);
1972     my $evt = lineitem_detail_CUD_batch($mgr, $li_details, $create_debits, $dry_run);
1973     return $evt if $evt;
1974     $e->commit;
1975     return $mgr->respond_complete;
1976 }
1977
1978
1979 sub lineitem_detail_CUD_batch {
1980     my($mgr, $li_details, $create_debits, $dry_run) = @_;
1981
1982     $mgr->total(scalar(@$li_details));
1983     my $e = $mgr->editor;
1984     
1985     my $li;
1986     my %li_cache;
1987     my $fund_cache = {};
1988     my $evt;
1989
1990     for my $lid (@$li_details) {
1991
1992         unless($li = $li_cache{$lid->lineitem}) {
1993             ($li, $evt) = fetch_and_check_li($e, $lid->lineitem, 'write');
1994             return $evt if $evt;
1995         }
1996
1997         if($lid->isnew) {
1998             $lid = create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
1999             if($create_debits) {
2000                 $li->provider($e->retrieve_acq_provider($li->provider)) or return $e->die_event;
2001                 $lid->fund($e->retrieve_acq_fund($lid->fund)) or return $e->die_event;
2002                 create_lineitem_detail_debit($mgr, $li, $lid, 0, 1) or return $e->die_event;
2003             }
2004
2005         } elsif($lid->ischanged) {
2006             return $evt if $evt = handle_changed_lid($e, $lid, $dry_run, $fund_cache);
2007
2008         } elsif($lid->isdeleted) {
2009             delete_lineitem_detail($mgr, $lid) or return $e->die_event;
2010         }
2011
2012         $mgr->respond(li => $li);
2013         $li_cache{$lid->lineitem} = $li;
2014     }
2015
2016     return undef;
2017 }
2018
2019 sub handle_changed_lid {
2020     my($e, $lid, $dry_run, $fund_cache) = @_;
2021
2022     my $orig_lid = $e->retrieve_acq_lineitem_detail($lid->id) or return $e->die_event;
2023
2024     # updating the fund, so update the debit
2025     if($orig_lid->fund_debit and $orig_lid->fund != $lid->fund) {
2026
2027         my $debit = $e->retrieve_acq_fund_debit($orig_lid->fund_debit);
2028         my $new_fund = $$fund_cache{$lid->fund} = 
2029             $$fund_cache{$lid->fund} || $e->retrieve_acq_fund($lid->fund);
2030
2031         # check the thresholds
2032         return $e->die_event if
2033             fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "stop");
2034         return $e->die_event if $dry_run and 
2035             fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "warning");
2036
2037         $debit->fund($new_fund->id);
2038         $e->update_acq_fund_debit($debit) or return $e->die_event;
2039     }
2040
2041     $e->update_acq_lineitem_detail($lid) or return $e->die_event;
2042     return undef;
2043 }
2044
2045
2046 __PACKAGE__->register_method(
2047     method   => 'receive_po_api',
2048     api_name => 'open-ils.acq.purchase_order.receive'
2049 );
2050
2051 sub receive_po_api {
2052     my($self, $conn, $auth, $po_id) = @_;
2053     my $e = new_editor(xact => 1, authtoken => $auth);
2054     return $e->die_event unless $e->checkauth;
2055     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2056
2057     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2058     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2059
2060     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2061
2062     for my $li_id (@$li_ids) {
2063         receive_lineitem($mgr, $li_id) or return $e->die_event;
2064         $mgr->respond;
2065     }
2066
2067     $po->state('received');
2068     update_purchase_order($mgr, $po) or return $e->die_event;
2069
2070     $e->commit;
2071     return $mgr->respond_complete;
2072 }
2073
2074
2075 # At the moment there's a lack of parallelism between the receive and unreceive
2076 # API methods for POs and the API methods for LIs and LIDs.  The methods for
2077 # POs stream back objects as they act, whereas the methods for LIs and LIDs
2078 # atomically return an object that describes only what changed (in LIs and LIDs
2079 # themselves or in the objects to which to LIs and LIDs belong).
2080 #
2081 # The methods for LIs and LIDs work the way they do to faciliate the UI's
2082 # maintaining correct information about the state of these things when a user
2083 # wants to receive or unreceive these objects without refreshing their whole
2084 # display.  The UI feature for receiving and un-receiving a whole PO just
2085 # refreshes the whole display, so this absence of parallelism in the UI is also
2086 # relected in this module.
2087 #
2088 # This could be neatened in the future by making POs receive and unreceive in
2089 # the same way the LIs and LIDs do.
2090
2091 __PACKAGE__->register_method(
2092     method => 'receive_lineitem_detail_api',
2093     api_name    => 'open-ils.acq.lineitem_detail.receive',
2094     signature => {
2095         desc => 'Mark a lineitem_detail as received',
2096         params => [
2097             {desc => 'Authentication token', type => 'string'},
2098             {desc => 'lineitem detail ID', type => 'number'}
2099         ],
2100         return => {desc =>
2101             "on success, object describing changes to LID and possibly " .
2102             "to LI and PO; on error, Event"
2103         }
2104     }
2105 );
2106
2107 sub receive_lineitem_detail_api {
2108     my($self, $conn, $auth, $lid_id) = @_;
2109
2110     my $e = new_editor(xact=>1, authtoken=>$auth);
2111     return $e->die_event unless $e->checkauth;
2112     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2113
2114     my $fleshing = {
2115         "flesh" => 2, "flesh_fields" => {
2116             "acqlid" => ["lineitem"], "jub" => ["purchase_order"]
2117         }
2118     };
2119
2120     my $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2121
2122     return $e->die_event unless $e->allowed(
2123         'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
2124
2125     # update ...
2126     my $recvd = receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
2127
2128     # .. and re-retrieve
2129     $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2130
2131     # Now build result data structure.
2132     my $result = {"lid" => {$lid->id => {"recv_time" => $lid->recv_time}}};
2133
2134     if (ref $recvd) {
2135         if ($recvd->class_name =~ /::purchase_order/) {
2136             $result->{"po"} = describe_affected_po($e, $recvd);
2137             $result->{"li"} = {
2138                 $lid->lineitem->id => {"state" => $lid->lineitem->state}
2139             };
2140         } elsif ($recvd->class_name =~ /::lineitem/) {
2141             $result->{"li"} = {$recvd->id => {"state" => $recvd->state}};
2142         }
2143     }
2144     $result->{"po"} ||=
2145         describe_affected_po($e, $lid->lineitem->purchase_order);
2146
2147     $e->commit;
2148     return $result;
2149 }
2150
2151 __PACKAGE__->register_method(
2152     method => 'receive_lineitem_api',
2153     api_name    => 'open-ils.acq.lineitem.receive',
2154     signature => {
2155         desc => 'Mark a lineitem as received',
2156         params => [
2157             {desc => 'Authentication token', type => 'string'},
2158             {desc => 'lineitem ID', type => 'number'}
2159         ],
2160         return => {desc =>
2161             "on success, object describing changes to LI and possibly PO; " .
2162             "on error, Event"
2163         }
2164     }
2165 );
2166
2167 sub receive_lineitem_api {
2168     my($self, $conn, $auth, $li_id) = @_;
2169
2170     my $e = new_editor(xact=>1, authtoken=>$auth);
2171     return $e->die_event unless $e->checkauth;
2172     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2173
2174     my $li = $e->retrieve_acq_lineitem([
2175         $li_id, {
2176             flesh => 1,
2177             flesh_fields => {
2178                 jub => ['purchase_order']
2179             }
2180         }
2181     ]) or return $e->die_event;
2182
2183     return $e->die_event unless $e->allowed(
2184         'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
2185
2186     my $res = receive_lineitem($mgr, $li_id) or return $e->die_event;
2187     $e->commit;
2188     $conn->respond_complete($res);
2189     $mgr->run_post_response_hooks
2190 }
2191
2192
2193 __PACKAGE__->register_method(
2194     method => 'receive_lineitem_batch_api',
2195     api_name    => 'open-ils.acq.lineitem.receive.batch',
2196     stream => 1,
2197     signature => {
2198         desc => 'Mark lineitems as received',
2199         params => [
2200             {desc => 'Authentication token', type => 'string'},
2201             {desc => 'lineitem ID list', type => 'array'}
2202         ],
2203         return => {desc =>
2204             q/On success, stream of objects describing changes to LIs and
2205             possibly PO; onerror, Event.  Any event, even after lots of other
2206             objects, should mean general failure of whole batch operation./
2207         }
2208     }
2209 );
2210
2211 sub receive_lineitem_batch_api {
2212     my ($self, $conn, $auth, $li_idlist) = @_;
2213
2214     return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2215
2216     my $e = new_editor(xact => 1, authtoken => $auth);
2217     return $e->die_event unless $e->checkauth;
2218
2219     my $mgr = new OpenILS::Application::Acq::BatchManager(
2220         editor => $e, conn => $conn
2221     );
2222
2223     for my $li_id (map { int $_ } @$li_idlist) {
2224         my $li = $e->retrieve_acq_lineitem([
2225             $li_id, {
2226                 flesh => 1,
2227                 flesh_fields => { jub => ['purchase_order'] }
2228             }
2229         ]) or return $e->die_event;
2230
2231         return $e->die_event unless $e->allowed(
2232             'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency
2233         );
2234
2235         receive_lineitem($mgr, $li_id) or return $e->die_event;
2236         $mgr->respond;
2237     }
2238
2239     $e->commit or return $e->die_event;
2240     $mgr->respond_complete;
2241     $mgr->run_post_response_hooks;
2242 }
2243
2244 __PACKAGE__->register_method(
2245     method   => 'rollback_receive_po_api',
2246     api_name => 'open-ils.acq.purchase_order.receive.rollback'
2247 );
2248
2249 sub rollback_receive_po_api {
2250     my($self, $conn, $auth, $po_id) = @_;
2251     my $e = new_editor(xact => 1, authtoken => $auth);
2252     return $e->die_event unless $e->checkauth;
2253     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2254
2255     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2256     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2257
2258     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2259
2260     for my $li_id (@$li_ids) {
2261         rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2262         $mgr->respond;
2263     }
2264
2265     $po->state('on-order');
2266     update_purchase_order($mgr, $po) or return $e->die_event;
2267
2268     $e->commit;
2269     return $mgr->respond_complete;
2270 }
2271
2272
2273 __PACKAGE__->register_method(
2274     method    => 'rollback_receive_lineitem_detail_api',
2275     api_name  => 'open-ils.acq.lineitem_detail.receive.rollback',
2276     signature => {
2277         desc   => 'Mark a lineitem_detail as Un-received',
2278         params => [
2279             {desc => 'Authentication token', type => 'string'},
2280             {desc => 'lineitem detail ID', type => 'number'}
2281         ],
2282         return => {desc =>
2283             "on success, object describing changes to LID and possibly " .
2284             "to LI and PO; on error, Event"
2285         }
2286     }
2287 );
2288
2289 sub rollback_receive_lineitem_detail_api {
2290     my($self, $conn, $auth, $lid_id) = @_;
2291
2292     my $e = new_editor(xact=>1, authtoken=>$auth);
2293     return $e->die_event unless $e->checkauth;
2294     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2295
2296     my $lid = $e->retrieve_acq_lineitem_detail([
2297         $lid_id, {
2298             flesh => 2,
2299             flesh_fields => {
2300                 acqlid => ['lineitem'],
2301                 jub => ['purchase_order']
2302             }
2303         }
2304     ]);
2305     my $li = $lid->lineitem;
2306     my $po = $li->purchase_order;
2307
2308     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2309
2310     my $result = {};
2311
2312     my $recvd = rollback_receive_lineitem_detail($mgr, $lid_id)
2313         or return $e->die_event;
2314
2315     if (ref $recvd) {
2316         $result->{"lid"} = {$recvd->id => {"recv_time" => $recvd->recv_time}};
2317     } else {
2318         $result->{"lid"} = {$lid->id => {"recv_time" => $lid->recv_time}};
2319     }
2320
2321     if ($li->state eq "received") {
2322         $li->state("on-order");
2323         $li = update_lineitem($mgr, $li) or return $e->die_event;
2324         $result->{"li"} = {$li->id => {"state" => $li->state}};
2325     }
2326
2327     if ($po->state eq "received") {
2328         $po->state("on-order");
2329         $po = update_purchase_order($mgr, $po) or return $e->die_event;
2330     }
2331     $result->{"po"} = describe_affected_po($e, $po);
2332
2333     $e->commit and return $result or return $e->die_event;
2334 }
2335
2336 __PACKAGE__->register_method(
2337     method    => 'rollback_receive_lineitem_api',
2338     api_name  => 'open-ils.acq.lineitem.receive.rollback',
2339     signature => {
2340         desc   => 'Mark a lineitem as Un-received',
2341         params => [
2342             {desc => 'Authentication token', type => 'string'},
2343             {desc => 'lineitem ID',          type => 'number'}
2344         ],
2345         return => {desc =>
2346             "on success, object describing changes to LI and possibly PO; " .
2347             "on error, Event"
2348         }
2349     }
2350 );
2351
2352 sub rollback_receive_lineitem_api {
2353     my($self, $conn, $auth, $li_id) = @_;
2354
2355     my $e = new_editor(xact=>1, authtoken=>$auth);
2356     return $e->die_event unless $e->checkauth;
2357     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2358
2359     my $li = $e->retrieve_acq_lineitem([
2360         $li_id, {
2361             "flesh" => 1, "flesh_fields" => {"jub" => ["purchase_order"]}
2362         }
2363     ]);
2364     my $po = $li->purchase_order;
2365
2366     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2367
2368     $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2369
2370     my $result = {"li" => {$li->id => {"state" => $li->state}}};
2371     if ($po->state eq "received") {
2372         $po->state("on-order");
2373         $po = update_purchase_order($mgr, $po) or return $e->die_event;
2374     }
2375     $result->{"po"} = describe_affected_po($e, $po);
2376
2377     $e->commit and return $result or return $e->die_event;
2378 }
2379
2380 __PACKAGE__->register_method(
2381     method    => 'rollback_receive_lineitem_batch_api',
2382     api_name  => 'open-ils.acq.lineitem.receive.rollback.batch',
2383     stream => 1,
2384     signature => {
2385         desc   => 'Mark a list of lineitems as Un-received',
2386         params => [
2387             {desc => 'Authentication token', type => 'string'},
2388             {desc => 'lineitem ID list',     type => 'array'}
2389         ],
2390         return => {desc =>
2391             q/on success, a stream of objects describing changes to LI and
2392             possibly PO; on error, Event. Any event means all previously
2393             returned objects indicate changes that didn't really happen./
2394         }
2395     }
2396 );
2397
2398 sub rollback_receive_lineitem_batch_api {
2399     my ($self, $conn, $auth, $li_idlist) = @_;
2400
2401     return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2402
2403     my $e = new_editor(xact => 1, authtoken => $auth);
2404     return $e->die_event unless $e->checkauth;
2405
2406     my $mgr = new OpenILS::Application::Acq::BatchManager(
2407         editor => $e, conn => $conn
2408     );
2409
2410     for my $li_id (map { int $_ } @$li_idlist) {
2411         my $li = $e->retrieve_acq_lineitem([
2412             $li_id, {
2413                 "flesh" => 1,
2414                 "flesh_fields" => {"jub" => ["purchase_order"]}
2415             }
2416         ]);
2417
2418         my $po = $li->purchase_order;
2419
2420         return $e->die_event unless
2421             $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2422
2423         $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2424
2425         my $result = {"li" => {$li->id => {"state" => $li->state}}};
2426         if ($po->state eq "received") { # should happen first time, not after
2427             $po->state("on-order");
2428             $po = update_purchase_order($mgr, $po) or return $e->die_event;
2429         }
2430         $result->{"po"} = describe_affected_po($e, $po);
2431
2432         $mgr->respond(%$result);
2433     }
2434
2435     $e->commit or return $e->die_event;
2436     $mgr->respond_complete;
2437     $mgr->run_post_response_hooks;
2438 }
2439
2440
2441 __PACKAGE__->register_method(
2442     method    => 'set_lineitem_price_api',
2443     api_name  => 'open-ils.acq.lineitem.price.set',
2444     signature => {
2445         desc   => 'Set lineitem price.  If debits already exist, update them as well',
2446         params => [
2447             {desc => 'Authentication token', type => 'string'},
2448             {desc => 'lineitem ID',          type => 'number'}
2449         ],
2450         return => {desc => 'status blob, Event on error'}
2451     }
2452 );
2453
2454 sub set_lineitem_price_api {
2455     my($self, $conn, $auth, $li_id, $price) = @_;
2456
2457     my $e = new_editor(xact=>1, authtoken=>$auth);
2458     return $e->die_event unless $e->checkauth;
2459     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2460
2461     my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
2462     return $evt if $evt;
2463
2464     $li->estimated_unit_price($price);
2465     update_lineitem($mgr, $li) or return $e->die_event;
2466
2467     my $lid_ids = $e->search_acq_lineitem_detail(
2468         {lineitem => $li_id, fund_debit => {'!=' => undef}}, 
2469         {idlist => 1}
2470     );
2471
2472     for my $lid_id (@$lid_ids) {
2473
2474         my $lid = $e->retrieve_acq_lineitem_detail([
2475             $lid_id, {
2476             flesh => 1, flesh_fields => {acqlid => ['fund', 'fund_debit']}}
2477         ]);
2478
2479         $lid->fund_debit->amount($price);
2480         $e->update_acq_fund_debit($lid->fund_debit) or return $e->die_event;
2481         $mgr->add_lid;
2482         $mgr->respond;
2483     }
2484
2485     $e->commit;
2486     return $mgr->respond_complete;
2487 }
2488
2489
2490 __PACKAGE__->register_method(
2491     method    => 'clone_picklist_api',
2492     api_name  => 'open-ils.acq.picklist.clone',
2493     signature => {
2494         desc   => 'Clones a picklist, including lineitem and lineitem details',
2495         params => [
2496             {desc => 'Authentication token', type => 'string'},
2497             {desc => 'Picklist ID', type => 'number'},
2498             {desc => 'New Picklist Name', type => 'string'}
2499         ],
2500         return => {desc => 'status blob, Event on error'}
2501     }
2502 );
2503
2504 sub clone_picklist_api {
2505     my($self, $conn, $auth, $pl_id, $name) = @_;
2506
2507     my $e = new_editor(xact=>1, authtoken=>$auth);
2508     return $e->die_event unless $e->checkauth;
2509     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2510
2511     my $old_pl = $e->retrieve_acq_picklist($pl_id);
2512     my $new_pl = create_picklist($mgr, %{$old_pl->to_bare_hash}, name => $name) or return $e->die_event;
2513
2514     my $li_ids = $e->search_acq_lineitem({picklist => $pl_id}, {idlist => 1});
2515
2516     # get the current user
2517     my $cloner = $mgr->editor->requestor->id;
2518
2519     for my $li_id (@$li_ids) {
2520
2521         # copy the lineitems' MARC
2522         my $marc = ($e->retrieve_acq_lineitem($li_id))->marc;
2523
2524         # create a skeletal clone of the item
2525         my $li = Fieldmapper::acq::lineitem->new;
2526         $li->creator($cloner);
2527         $li->selector($cloner);
2528         $li->editor($cloner);
2529         $li->marc($marc);
2530
2531         my $new_li = create_lineitem($mgr, %{$li->to_bare_hash}, picklist => $new_pl->id) or return $e->die_event;
2532
2533         $mgr->respond;
2534     }
2535
2536     $e->commit;
2537     return $mgr->respond_complete;
2538 }
2539
2540
2541 __PACKAGE__->register_method(
2542     method    => 'merge_picklist_api',
2543     api_name  => 'open-ils.acq.picklist.merge',
2544     signature => {
2545         desc   => 'Merges 2 or more picklists into a single list',
2546         params => [
2547             {desc => 'Authentication token', type => 'string'},
2548             {desc => 'Lead Picklist ID', type => 'number'},
2549             {desc => 'List of subordinate picklist IDs', type => 'array'}
2550         ],
2551         return => {desc => 'status blob, Event on error'}
2552     }
2553 );
2554
2555 sub merge_picklist_api {
2556     my($self, $conn, $auth, $lead_pl, $pl_list) = @_;
2557
2558     my $e = new_editor(xact=>1, authtoken=>$auth);
2559     return $e->die_event unless $e->checkauth;
2560     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2561
2562     # XXX perms on each picklist modified
2563
2564     $lead_pl = $e->retrieve_acq_picklist($lead_pl) or return $e->die_event;
2565     # point all of the lineitems at the lead picklist
2566     my $li_ids = $e->search_acq_lineitem({picklist => $pl_list}, {idlist => 1});
2567
2568     for my $li_id (@$li_ids) {
2569         my $li = $e->retrieve_acq_lineitem($li_id);
2570         $li->picklist($lead_pl);
2571         update_lineitem($mgr, $li) or return $e->die_event;
2572         $mgr->respond;
2573     }
2574
2575     # now delete the subordinate lists
2576     for my $pl_id (@$pl_list) {
2577         my $pl = $e->retrieve_acq_picklist($pl_id);
2578         $e->delete_acq_picklist($pl) or return $e->die_event;
2579     }
2580
2581     update_picklist($mgr, $lead_pl) or return $e->die_event;
2582
2583     $e->commit;
2584     return $mgr->respond_complete;
2585 }
2586
2587
2588 __PACKAGE__->register_method(
2589     method    => 'delete_picklist_api',
2590     api_name  => 'open-ils.acq.picklist.delete',
2591     signature => {
2592         desc   => q/Deletes a picklist.  It also deletes any lineitems in the "new" state. / .
2593                   q/Other attached lineitems are detached/,
2594         params => [
2595             {desc => 'Authentication token',  type => 'string'},
2596             {desc => 'Picklist ID to delete', type => 'number'}
2597         ],
2598         return => {desc => '1 on success, Event on error'}
2599     }
2600 );
2601
2602 sub delete_picklist_api {
2603     my($self, $conn, $auth, $picklist_id) = @_;
2604     my $e = new_editor(xact=>1, authtoken=>$auth);
2605     return $e->die_event unless $e->checkauth;
2606     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2607     my $pl = $e->retrieve_acq_picklist($picklist_id) or return $e->die_event;
2608     delete_picklist($mgr, $pl) or return $e->die_event;
2609     $e->commit;
2610     return $mgr->respond_complete;
2611 }
2612
2613
2614
2615 __PACKAGE__->register_method(
2616     method   => 'activate_purchase_order',
2617     api_name => 'open-ils.acq.purchase_order.activate.dry_run'
2618 );
2619
2620 __PACKAGE__->register_method(
2621     method    => 'activate_purchase_order',
2622     api_name  => 'open-ils.acq.purchase_order.activate',
2623     signature => {
2624         desc => q/Activates a purchase order.  This updates the status of the PO / .
2625                 q/and Lineitems to 'on-order'.  Activated PO's are ready for EDI delivery if appropriate./,
2626         params => [
2627             {desc => 'Authentication token', type => 'string'},
2628             {desc => 'Purchase ID', type => 'number'}
2629         ],
2630         return => {desc => '1 on success, Event on error'}
2631     }
2632 );
2633
2634 sub activate_purchase_order {
2635     my($self, $conn, $auth, $po_id, $vandelay, $options) = @_;
2636     $options ||= {};
2637     $$options{dry_run} = ($self->api_name =~ /\.dry_run/) ? 1 : 0;
2638
2639     my $e = new_editor(authtoken=>$auth);
2640     return $e->die_event unless $e->checkauth;
2641     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2642     my $die_event = activate_purchase_order_impl($mgr, $po_id, $vandelay, $options);
2643     return $e->die_event if $die_event;
2644     $conn->respond_complete(1);
2645     $mgr->run_post_response_hooks unless $$options{dry_run};
2646     return undef;
2647 }
2648
2649 # xacts managed within
2650 sub activate_purchase_order_impl {
2651     my ($mgr, $po_id, $vandelay, $options) = @_;
2652     $options ||= {};
2653     my $dry_run = $$options{dry_run};
2654     my $no_assets = $$options{no_assets};
2655
2656     # read-only until lineitem asset creation
2657     my $e = $mgr->editor;
2658     $e->xact_begin;
2659
2660     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2661     return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
2662
2663     return $e->die_event(OpenILS::Event->new('PO_ALREADY_ACTIVATED'))
2664         if $po->order_date; # PO cannot be re-activated
2665
2666     my $provider = $e->retrieve_acq_provider($po->provider);
2667
2668     # find lineitems and create assets for all
2669
2670     my $query = {   
2671         purchase_order => $po_id, 
2672         state => [qw/pending-order new order-ready/]
2673     };
2674
2675     my $li_ids = $e->search_acq_lineitem($query, {idlist => 1});
2676
2677     my $vl_resp; # imported li's and the managing queue
2678     unless ($dry_run or $no_assets) {
2679         $e->rollback; # read-only thus far
2680
2681         # list_assets manages its own transactions
2682         $vl_resp = create_lineitem_list_assets($mgr, $li_ids, $vandelay)
2683             or return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED');
2684         $e->xact_begin;
2685     }
2686
2687     # create fund debits for lineitems 
2688
2689     for my $li_id (@$li_ids) {
2690         my $li = $e->retrieve_acq_lineitem($li_id);
2691         
2692         unless ($li->eg_bib_id or $dry_run or $no_assets) {
2693             # we encountered a lineitem that was not successfully imported.
2694             # we cannot continue.  rollback and report.
2695             $e->rollback;
2696             return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED', {queue => $vl_resp->{queue}});
2697         }
2698
2699         $li->state('on-order');
2700         $li->claim_policy($provider->default_claim_policy)
2701             if $provider->default_claim_policy and !$li->claim_policy;
2702         create_lineitem_debits($mgr, $li, $options) or return $e->die_event;
2703         update_lineitem($mgr, $li) or return $e->die_event;
2704         $mgr->post_process( sub { create_lineitem_status_events($mgr, $li->id, 'aur.ordered'); });
2705         $mgr->respond;
2706     }
2707
2708     # create po-item debits
2709
2710     for my $po_item (@{$e->search_acq_po_item({purchase_order => $po_id})}) {
2711
2712         my $debit = create_fund_debit(
2713             $mgr, 
2714             $dry_run, 
2715             debit_type => 'direct_charge', # to match invoicing
2716             origin_amount => $po_item->estimated_cost,
2717             origin_currency_type => $e->retrieve_acq_fund($po_item->fund)->currency_type,
2718             amount => $po_item->estimated_cost,
2719             fund => $po_item->fund
2720         ) or return $e->die_event;
2721         $po_item->fund_debit($debit->id);
2722         $e->update_acq_po_item($po_item) or return $e->die_event;
2723         $mgr->respond;
2724     }
2725
2726     # mark PO as ordered
2727
2728     $po->state('on-order');
2729     $po->order_date('now');
2730     update_purchase_order($mgr, $po) or return $e->die_event;
2731
2732     # clean up the xact
2733     $dry_run and $e->rollback or $e->commit;
2734
2735     # tell the world we activated a PO
2736     $U->create_events_for_hook('acqpo.activated', $po, $po->ordering_agency) unless $dry_run;
2737
2738     return undef;
2739 }
2740
2741
2742 __PACKAGE__->register_method(
2743     method    => 'split_purchase_order_by_lineitems',
2744     api_name  => 'open-ils.acq.purchase_order.split_by_lineitems',
2745     signature => {
2746         desc   => q/Splits a PO into many POs, 1 per lineitem.  Only works for / .
2747                   q/POs a) with more than one lineitems, and b) in the "pending" state./,
2748         params => [
2749             {desc => 'Authentication token', type => 'string'},
2750             {desc => 'Purchase order ID',    type => 'number'}
2751         ],
2752         return => {desc => 'list of new PO IDs on success, Event on error'}
2753     }
2754 );
2755
2756 sub split_purchase_order_by_lineitems {
2757     my ($self, $conn, $auth, $po_id) = @_;
2758
2759     my $e = new_editor("xact" => 1, "authtoken" => $auth);
2760     return $e->die_event unless $e->checkauth;
2761
2762     my $po = $e->retrieve_acq_purchase_order([
2763         $po_id, {
2764             "flesh" => 1,
2765             "flesh_fields" => {"acqpo" => [qw/lineitems notes/]}
2766         }
2767     ]) or return $e->die_event;
2768
2769     return $e->die_event
2770         unless $e->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2771
2772     unless ($po->state eq "pending") {
2773         $e->rollback;
2774         return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_LATE");
2775     }
2776
2777     unless (@{$po->lineitems} > 1) {
2778         $e->rollback;
2779         return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_SHORT");
2780     }
2781
2782     # To split an existing PO into many, it seems unwise to just delete the
2783     # original PO, so we'll instead detach all of the original POs' lineitems
2784     # but the first, then create new POs for each of the remaining LIs, and
2785     # then attach the LIs to their new POs.
2786
2787     my @po_ids = ($po->id);
2788     my @moving_li = @{$po->lineitems};
2789     shift @moving_li;    # discard first LI
2790
2791     foreach my $li (@moving_li) {
2792         my $new_po = $po->clone;
2793         $new_po->clear_id;
2794         $new_po->clear_name;
2795         $new_po->creator($e->requestor->id);
2796         $new_po->editor($e->requestor->id);
2797         $new_po->owner($e->requestor->id);
2798         $new_po->edit_time("now");
2799         $new_po->create_time("now");
2800
2801         $new_po = $e->create_acq_purchase_order($new_po);
2802
2803         # Clone any notes attached to the old PO and attach to the new one.
2804         foreach my $note (@{$po->notes}) {
2805             my $new_note = $note->clone;
2806             $new_note->clear_id;
2807             $new_note->edit_time("now");
2808             $new_note->purchase_order($new_po->id);
2809             $e->create_acq_po_note($new_note);
2810         }
2811
2812         $li->edit_time("now");
2813         $li->purchase_order($new_po->id);
2814         $e->update_acq_lineitem($li);
2815
2816         push @po_ids, $new_po->id;
2817     }
2818
2819     $po->edit_time("now");
2820     $e->update_acq_purchase_order($po);
2821
2822     return \@po_ids if $e->commit;
2823     return $e->die_event;
2824 }
2825
2826
2827 sub not_cancelable {
2828     my $o = shift;
2829     (ref $o eq "HASH" and $o->{"textcode"} eq "ACQ_NOT_CANCELABLE");
2830 }
2831
2832 __PACKAGE__->register_method(
2833     method => "cancel_purchase_order_api",
2834     api_name    => "open-ils.acq.purchase_order.cancel",
2835     signature => {
2836         desc => q/Cancels an on-order purchase order/,
2837         params => [
2838             {desc => "Authentication token", type => "string"},
2839             {desc => "PO ID to cancel", type => "number"},
2840             {desc => "Cancel reason ID", type => "number"}
2841         ],
2842         return => {desc => q/Object describing changed POs, LIs and LIDs
2843             on success; Event on error./}
2844     }
2845 );
2846
2847 sub cancel_purchase_order_api {
2848     my ($self, $conn, $auth, $po_id, $cancel_reason) = @_;
2849
2850     my $e = new_editor("xact" => 1, "authtoken" => $auth);
2851     return $e->die_event unless $e->checkauth;
2852     my $mgr = new OpenILS::Application::Acq::BatchManager(
2853         "editor" => $e, "conn" => $conn
2854     );
2855
2856     $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
2857         return new OpenILS::Event(
2858             "BAD_PARAMS", "note" => "Provide cancel reason ID"
2859         );
2860
2861     my $result = cancel_purchase_order($mgr, $po_id, $cancel_reason) or
2862         return $e->die_event;
2863     if (not_cancelable($result)) { # event not from CStoreEditor
2864         $e->rollback;
2865         return $result;
2866     } elsif ($result == -1) {
2867         $e->rollback;
2868         return new OpenILS::Event("ACQ_ALREADY_CANCELED");
2869     }
2870
2871     $e->commit or return $e->die_event;
2872
2873     # XXX create purchase order status events?
2874
2875     if ($mgr->{post_commit}) {
2876         foreach my $func (@{$mgr->{post_commit}}) {
2877             $func->();
2878         }
2879     }
2880
2881     return $result;
2882 }
2883
2884 sub cancel_purchase_order {
2885     my ($mgr, $po_id, $cancel_reason) = @_;
2886
2887     my $po = $mgr->editor->retrieve_acq_purchase_order($po_id) or return 0;
2888
2889     # XXX is "cancelled" a typo?  It's not correct US spelling, anyway.
2890     # Depending on context, this may not warrant an event.
2891     return -1 if $po->state eq "cancelled";
2892
2893     # But this always does.
2894     return new OpenILS::Event(
2895         "ACQ_NOT_CANCELABLE", "note" => "purchase_order $po_id"
2896     ) unless ($po->state eq "on-order" or $po->state eq "pending");
2897
2898     return 0 unless
2899         $mgr->editor->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2900
2901     $po->state("cancelled");
2902     $po->cancel_reason($cancel_reason->id);
2903
2904     my $li_ids = $mgr->editor->search_acq_lineitem(
2905         {"purchase_order" => $po_id}, {"idlist" => 1}
2906     );
2907
2908     my $result = {"li" => {}, "lid" => {}};
2909     foreach my $li_id (@$li_ids) {
2910         my $li_result = cancel_lineitem($mgr, $li_id, $cancel_reason)
2911             or return 0;
2912
2913         next if $li_result == -1; # already canceled:skip.
2914         return $li_result if not_cancelable($li_result); # not cancelable:stop.
2915
2916         # Merge in each LI result (there's only going to be
2917         # one per call to cancel_lineitem).
2918         my ($k, $v) = each %{$li_result->{"li"}};
2919         $result->{"li"}->{$k} = $v;
2920
2921         # Merge in each LID result (there may be many per call to
2922         # cancel_lineitem).
2923         while (($k, $v) = each %{$li_result->{"lid"}}) {
2924             $result->{"lid"}->{$k} = $v;
2925         }
2926     }
2927
2928     # TODO who/what/where/how do we indicate this change for electronic orders?
2929     # TODO return changes to encumbered/spent
2930     # TODO maybe cascade up from smaller object to container object if last
2931     # smaller object in the container has been canceled?
2932
2933     update_purchase_order($mgr, $po) or return 0;
2934     $result->{"po"} = {
2935         $po_id => {"state" => $po->state, "cancel_reason" => $cancel_reason}
2936     };
2937     return $result;
2938 }
2939
2940
2941 __PACKAGE__->register_method(
2942     method => "cancel_lineitem_api",
2943     api_name    => "open-ils.acq.lineitem.cancel",
2944     signature => {
2945         desc => q/Cancels an on-order lineitem/,
2946         params => [
2947             {desc => "Authentication token", type => "string"},
2948             {desc => "Lineitem ID to cancel", type => "number"},
2949             {desc => "Cancel reason ID", type => "number"}
2950         ],
2951         return => {desc => q/Object describing changed LIs and LIDs on success;
2952             Event on error./}
2953     }
2954 );
2955
2956 __PACKAGE__->register_method(
2957     method => "cancel_lineitem_api",
2958     api_name    => "open-ils.acq.lineitem.cancel.batch",
2959     signature => {
2960         desc => q/Batched version of open-ils.acq.lineitem.cancel/,
2961         return => {desc => q/Object describing changed LIs and LIDs on success;
2962             Event on error./}
2963     }
2964 );
2965
2966 sub cancel_lineitem_api {
2967     my ($self, $conn, $auth, $li_id, $cancel_reason) = @_;
2968
2969     my $batched = $self->api_name =~ /\.batch/;
2970
2971     my $e = new_editor("xact" => 1, "authtoken" => $auth);
2972     return $e->die_event unless $e->checkauth;
2973     my $mgr = new OpenILS::Application::Acq::BatchManager(
2974         "editor" => $e, "conn" => $conn
2975     );
2976
2977     $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
2978         return new OpenILS::Event(
2979             "BAD_PARAMS", "note" => "Provide cancel reason ID"
2980         );
2981
2982     my ($result, $maybe_event);
2983
2984     if ($batched) {
2985         $result = {"li" => {}, "lid" => {}};
2986         foreach my $one_li_id (@$li_id) {
2987             my $one = cancel_lineitem($mgr, $one_li_id, $cancel_reason) or
2988                 return $e->die_event;
2989             if (not_cancelable($one)) {
2990                 $maybe_event = $one;
2991             } elsif ($result == -1) {
2992                 $maybe_event = new OpenILS::Event("ACQ_ALREADY_CANCELED");
2993             } else {
2994                 my ($k, $v);
2995                 if ($one->{"li"}) {
2996                     while (($k, $v) = each %{$one->{"li"}}) {
2997                         $result->{"li"}->{$k} = $v;
2998                     }
2999                 }
3000                 if ($one->{"lid"}) {
3001                     while (($k, $v) = each %{$one->{"lid"}}) {
3002                         $result->{"lid"}->{$k} = $v;
3003                     }
3004                 }
3005             }
3006         }
3007     } else {
3008         $result = cancel_lineitem($mgr, $li_id, $cancel_reason) or
3009             return $e->die_event;
3010
3011         if (not_cancelable($result)) {
3012             $e->rollback;
3013             return $result;
3014         } elsif ($result == -1) {
3015             $e->rollback;
3016             return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3017         }
3018     }
3019
3020     if ($batched and not scalar keys %{$result->{"li"}}) {
3021         $e->rollback;
3022         return $maybe_event;
3023     } else {
3024         $e->commit or return $e->die_event;
3025         # create_lineitem_status_events should handle array li_id ok
3026         create_lineitem_status_events($mgr, $li_id, "aur.cancelled");
3027
3028         if ($mgr->{post_commit}) {
3029             foreach my $func (@{$mgr->{post_commit}}) {
3030                 $func->();
3031             }
3032         }
3033
3034         return $result;
3035     }
3036 }
3037
3038 sub cancel_lineitem {
3039     my ($mgr, $li_id, $cancel_reason) = @_;
3040
3041     my $li = $mgr->editor->retrieve_acq_lineitem([
3042         $li_id, {flesh => 1, 
3043             flesh_fields => {jub => ['purchase_order','cancel_reason']}}
3044     ]) or return 0;
3045
3046     return 0 unless $mgr->editor->allowed(
3047         "CREATE_PURCHASE_ORDER", $li->purchase_order->ordering_agency
3048     );
3049
3050     # Depending on context, this may not warrant an event.
3051     return -1 if $li->state eq "cancelled" 
3052         and $li->cancel_reason->keep_debits eq 'f';
3053
3054     # But this always does.  Note that this used to be looser, but you can
3055     # no longer cancel lineitems that lack a PO or that are in "pending-order"
3056     # state (you could in the past).
3057     return new OpenILS::Event(
3058         "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id"
3059     ) unless $li->purchase_order and 
3060         ($li->state eq "on-order" or $li->state eq "cancelled");
3061
3062     $li->state("cancelled");
3063     $li->cancel_reason($cancel_reason->id);
3064
3065     my $lids = $mgr->editor->search_acq_lineitem_detail([{
3066         "lineitem" => $li_id
3067     }, {
3068         flesh => 1,
3069         flesh_fields => { acqlid => ['eg_copy_id'] }
3070     }]);
3071
3072     my $result = {"lid" => {}};
3073     my $copies = [];
3074     foreach my $lid (@$lids) {
3075         my $lid_result = cancel_lineitem_detail($mgr, $lid->id, $cancel_reason)
3076             or return 0;
3077
3078         # gathering any real copies for deletion
3079         if ($lid->eg_copy_id) {
3080             $lid->eg_copy_id->isdeleted('t');
3081             push @$copies, $lid->eg_copy_id;
3082         }
3083
3084         next if $lid_result == -1; # already canceled: just skip it.
3085         return $lid_result if not_cancelable($lid_result); # not cxlable: stop.
3086
3087         # Merge in each LID result (there's only going to be one per call to
3088         # cancel_lineitem_detail).
3089         my ($k, $v) = each %{$lid_result->{"lid"}};
3090         $result->{"lid"}->{$k} = $v;
3091     }
3092
3093     # Attempt to delete the gathered copies (this will also handle volume deletion and bib deletion)
3094     # Delete empty bibs according org unit setting
3095     my $force_delete_empty_bib = $U->ou_ancestor_setting_value(
3096         $mgr->editor->requestor->ws_ou, 'cat.bib.delete_on_no_copy_via_acq_lineitem_cancel', $mgr->editor);
3097     if (scalar(@$copies)>0) {
3098         my $override = 1;
3099         my $delete_stats = undef;
3100         my $retarget_holds = [];
3101         my $cat_evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
3102             $mgr->editor, $override, undef, $copies, $delete_stats, $retarget_holds,$force_delete_empty_bib);
3103
3104         if( $cat_evt ) {
3105             $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($cat_evt));
3106             return new OpenILS::Event(
3107                 "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id", "payload" => $cat_evt
3108             );
3109         }
3110
3111         # We can't do the following and stay within the same transaction, but that's okay, the hold targeter will pick these up later.
3112         #my $ses = OpenSRF::AppSession->create('open-ils.circ');
3113         #$ses->request('open-ils.circ.hold.reset.batch', $auth, $retarget_holds);
3114     }
3115
3116     # if we have a bib, check to see whether it has been deleted.  if so, cancel any active holds targeting that bib
3117     if ($li->eg_bib_id) {
3118         my $bib = $mgr->editor->retrieve_biblio_record_entry($li->eg_bib_id) or return new OpenILS::Event(
3119             "ACQ_NOT_CANCELABLE", "note" => "Could not retrieve bib " . $li->eg_bib_id . " for lineitem $li_id"
3120         );
3121         if ($U->is_true($bib->deleted)) {
3122             my $holds = $mgr->editor->search_action_hold_request(
3123                 {   cancel_time => undef,
3124                     fulfillment_time => undef,
3125                     target => $li->eg_bib_id
3126                 }
3127             );
3128
3129             my %cached_usr_home_ou = ();
3130
3131             for my $hold (@$holds) {
3132
3133                 $logger->info("Cancelling hold ".$hold->id.
3134                     " due to acq lineitem cancellation.");
3135
3136                 $hold->cancel_time('now');
3137                 $hold->cancel_cause(5); # 'Staff forced'--we may want a new hold cancel cause reason for this
3138                 $hold->cancel_note('Corresponding Acquistion Lineitem/Purchase Order was cancelled.');
3139                 unless($mgr->editor->update_action_hold_request($hold)) {
3140                     my $evt = $mgr->editor->event;
3141                     $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
3142                     return new OpenILS::Event(
3143                         "ACQ_NOT_CANCELABLE", "note" => "Could not cancel hold " . $hold->id . " for lineitem $li_id", "payload" => $evt
3144                     );
3145                 }
3146                 if (! defined $mgr->{post_commit}) { # we need a mechanism for creating trigger events, but only if the transaction gets committed
3147                     $mgr->{post_commit} = [];
3148                 }
3149                 push @{ $mgr->{post_commit} }, sub {
3150                     my $home_ou = $cached_usr_home_ou{$hold->usr};
3151                     if (! $home_ou) {
3152                         my $user = $mgr->editor->retrieve_actor_user($hold->usr); # FIXME: how do we want to handle failures here?
3153                         $home_ou = $user->home_ou;
3154                         $cached_usr_home_ou{$hold->usr} = $home_ou;
3155                     }
3156                     $U->create_events_for_hook('hold_request.cancel.cancelled_order', $hold, $home_ou);
3157                 };
3158             }
3159         }
3160     }
3161
3162     update_lineitem($mgr, $li) or return 0;
3163     $result->{"li"} = {
3164         $li_id => {
3165             "state" => $li->state,
3166             "cancel_reason" => $cancel_reason
3167         }
3168     };
3169     return $result;
3170 }
3171
3172
3173 __PACKAGE__->register_method(
3174     method => "cancel_lineitem_detail_api",
3175     api_name    => "open-ils.acq.lineitem_detail.cancel",
3176     signature => {
3177         desc => q/Cancels an on-order lineitem detail/,
3178         params => [
3179             {desc => "Authentication token", type => "string"},
3180             {desc => "Lineitem detail ID to cancel", type => "number"},
3181             {desc => "Cancel reason ID", type => "number"}
3182         ],
3183         return => {desc => q/Object describing changed LIDs on success;
3184             Event on error./}
3185     }
3186 );
3187
3188 sub cancel_lineitem_detail_api {
3189     my ($self, $conn, $auth, $lid_id, $cancel_reason) = @_;
3190
3191     my $e = new_editor("xact" => 1, "authtoken" => $auth);
3192     return $e->die_event unless $e->checkauth;
3193     my $mgr = new OpenILS::Application::Acq::BatchManager(
3194         "editor" => $e, "conn" => $conn
3195     );
3196
3197     $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
3198         return new OpenILS::Event(
3199             "BAD_PARAMS", "note" => "Provide cancel reason ID"
3200         );
3201
3202     my $result = cancel_lineitem_detail($mgr, $lid_id, $cancel_reason) or
3203         return $e->die_event;
3204
3205     if (not_cancelable($result)) {
3206         $e->rollback;
3207         return $result;
3208     } elsif ($result == -1) {
3209         $e->rollback;
3210         return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3211     }
3212
3213     $e->commit or return $e->die_event;
3214
3215     # XXX create lineitem detail status events?
3216     return $result;
3217 }
3218
3219 sub cancel_lineitem_detail {
3220     my ($mgr, $lid_id, $cancel_reason) = @_;
3221     my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
3222         $lid_id, {
3223             "flesh" => 2,
3224             "flesh_fields" => {
3225                 "acqlid" => ["lineitem","cancel_reason"], 
3226                 "jub" => ["purchase_order"]
3227             }
3228         }
3229     ]) or return 0;
3230
3231     # It's OK to cancel an already-canceled copy if the copy was
3232     # previously "delayed" -- keep_debits == true
3233     # Depending on context, this may not warrant an event.
3234     return -1 if $lid->cancel_reason 
3235         and $lid->cancel_reason->keep_debits eq 'f';
3236
3237     # But this always does.
3238     return new OpenILS::Event(
3239         "ACQ_NOT_CANCELABLE", "note" => "lineitem_detail $lid_id"
3240     ) unless (
3241         (! $lid->lineitem->purchase_order) or
3242         (
3243             (not $lid->recv_time) and
3244             $lid->lineitem and
3245             $lid->lineitem->purchase_order and (
3246                 $lid->lineitem->state eq "on-order" or
3247                 $lid->lineitem->state eq "pending-order" or
3248                 $lid->lineitem->state eq "cancelled"
3249             )
3250         )
3251     );
3252
3253     return 0 unless $mgr->editor->allowed(
3254         "CREATE_PURCHASE_ORDER",
3255         $lid->lineitem->purchase_order->ordering_agency
3256     ) or (! $lid->lineitem->purchase_order);
3257
3258     $lid->cancel_reason($cancel_reason->id);
3259
3260     unless($U->is_true($cancel_reason->keep_debits)) {
3261         my $debit_id = $lid->fund_debit;
3262         $lid->clear_fund_debit;
3263
3264         if($debit_id) {
3265             # item is cancelled.  Remove the fund debit.
3266             my $debit = $mgr->editor->retrieve_acq_fund_debit($debit_id);
3267             if (!$U->is_true($debit->encumbrance)) {
3268                 $mgr->editor->rollback;
3269                 return OpenILS::Event->new('ACQ_NOT_CANCELABLE', 
3270                     note => "Debit is marked as paid: $debit_id");
3271             }
3272             $mgr->editor->delete_acq_fund_debit($debit) or return $mgr->editor->die_event;
3273         }
3274     }
3275
3276     # XXX LIDs don't have either an editor or a edit_time field. Should we
3277     # update these on the LI when we alter an LID?
3278     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
3279
3280     return {"lid" => {$lid_id => {"cancel_reason" => $cancel_reason}}};
3281 }
3282
3283
3284 __PACKAGE__->register_method(
3285     method    => 'user_requests',
3286     api_name  => 'open-ils.acq.user_request.retrieve.by_user_id',
3287     stream    => 1,
3288     signature => {
3289         desc   => 'Retrieve fleshed user requests and related data for a given user.',
3290         params => [
3291             { desc => 'Authentication token',      type => 'string' },
3292             { desc => 'User ID of the owner, or array of IDs',      },
3293             { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3294               type => 'object'
3295             }
3296         ],
3297         return => {
3298             desc => 'Fleshed user requests and related data',
3299             type => 'object'
3300         }
3301     }
3302 );
3303
3304 __PACKAGE__->register_method(
3305     method    => 'user_requests',
3306     api_name  => 'open-ils.acq.user_request.retrieve.by_home_ou',
3307     stream    => 1,
3308     signature => {
3309         desc   => 'Retrieve fleshed user requests and related data for a given org unit or units.',
3310         params => [
3311             { desc => 'Authentication token',      type => 'string' },
3312             { desc => 'Org unit ID, or array of IDs',               },
3313             { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3314               type => 'object'
3315             }
3316         ],
3317         return => {
3318             desc => 'Fleshed user requests and related data',
3319             type => 'object'
3320         }
3321     }
3322 );
3323
3324 sub user_requests {
3325     my($self, $conn, $auth, $search_value, $options) = @_;
3326     my $e = new_editor(authtoken => $auth);
3327     return $e->event unless $e->checkauth;
3328     my $rid = $e->requestor->id;
3329     $options ||= {};
3330
3331     my $query = {
3332         "select"=>{"aur"=>["id"],"au"=>["home_ou", {column => 'id', alias => 'usr_id'} ]},
3333         "from"=>{ "aur" => { "au" => {}, "jub" => { "type" => "left" } } },
3334         "where"=>{
3335             "+jub"=> {
3336                 "-or" => [
3337                     {"id"=>undef}, # this with the left-join pulls in requests without lineitems
3338                     {"state"=>["new","on-order","pending-order"]} # FIXME - probably needs softcoding
3339                 ]
3340             }
3341         },
3342         "order_by"=>[{"class"=>"aur", "field"=>"request_date", "direction"=>"desc"}]
3343     };
3344
3345     foreach (qw/ order_by limit offset /) {
3346         $query->{$_} = $options->{$_} if defined $options->{$_};
3347     }
3348     if (defined $options->{'state'}) {
3349         $query->{'where'}->{'+jub'}->{'-or'}->[1]->{'state'} = $options->{'state'};        
3350     }
3351
3352     if ($self->api_name =~ /by_user_id/) {
3353         $query->{'where'}->{'usr'} = $search_value;
3354     } else {
3355         $query->{'where'}->{'+au'} = { 'home_ou' => $search_value };
3356     }
3357
3358     my $pertinent_ids = $e->json_query($query);
3359
3360     my %perm_test = ();
3361     for my $id_blob (@$pertinent_ids) {
3362         if ($rid != $id_blob->{usr_id}) {
3363             if (!defined $perm_test{ $id_blob->{home_ou} }) {
3364                 $perm_test{ $id_blob->{home_ou} } = $e->allowed( ['user_request.view'], $id_blob->{home_ou} );
3365             }
3366             if (!$perm_test{ $id_blob->{home_ou} }) {
3367                 next; # failed test
3368             }
3369         }
3370         my $aur_obj = $e->retrieve_acq_user_request([
3371             $id_blob->{id},
3372             {flesh => 1, flesh_fields => { "aur" => [ 'lineitem' ] } }
3373         ]);
3374         if (! $aur_obj) { next; }
3375
3376         if ($aur_obj->lineitem()) {
3377             $aur_obj->lineitem()->clear_marc();
3378         }
3379         $conn->respond($aur_obj);
3380     }
3381
3382     return undef;
3383 }
3384
3385 __PACKAGE__->register_method (
3386     method    => 'update_user_request',
3387     api_name  => 'open-ils.acq.user_request.cancel.batch',
3388     stream    => 1,
3389     signature => {
3390         desc   => 'If given a cancel reason, will update the request with that reason, otherwise, this will delete the request altogether.  The '    .
3391                   'intention is for staff interfaces or processes to provide cancel reasons, and for patron interfaces to just delete the requests.' ,
3392         params => [
3393             { desc => 'Authentication token',              type => 'string' },
3394             { desc => 'ID or array of IDs for the user requests to cancel'  },
3395             { desc => 'Cancel Reason ID (optional)',       type => 'string' }
3396         ],
3397         return => {
3398             desc => 'progress object, event on error',
3399         }
3400     }
3401 );
3402 __PACKAGE__->register_method (
3403     method    => 'update_user_request',
3404     api_name  => 'open-ils.acq.user_request.set_no_hold.batch',
3405     stream    => 1,
3406     signature => {
3407         desc   => 'Remove the hold from a user request or set of requests',
3408         params => [
3409             { desc => 'Authentication token',              type => 'string' },
3410             { desc => 'ID or array of IDs for the user requests to modify'  }
3411         ],
3412         return => {
3413             desc => 'progress object, event on error',
3414         }
3415     }
3416 );
3417
3418 sub update_user_request {
3419     my($self, $conn, $auth, $aur_ids, $cancel_reason) = @_;
3420     my $e = new_editor(xact => 1, authtoken => $auth);
3421     return $e->die_event unless $e->checkauth;
3422     my $rid = $e->requestor->id;
3423
3424     my $x = 1;
3425     my %perm_test = ();
3426     for my $id (@$aur_ids) {
3427
3428         my $aur_obj = $e->retrieve_acq_user_request([
3429             $id,
3430             {   flesh => 1,
3431                 flesh_fields => { "aur" => ['lineitem', 'usr'] }
3432             }
3433         ]) or return $e->die_event;
3434
3435         my $context_org = $aur_obj->usr()->home_ou();
3436         $aur_obj->usr( $aur_obj->usr()->id() );
3437
3438         if ($rid != $aur_obj->usr) {
3439             if (!defined $perm_test{ $context_org }) {
3440                 $perm_test{ $context_org } = $e->allowed( ['user_request.update'], $context_org );
3441             }
3442             if (!$perm_test{ $context_org }) {
3443                 next; # failed test
3444             }
3445         }
3446
3447         if($self->api_name =~ /set_no_hold/) {
3448             if ($U->is_true($aur_obj->hold)) { 
3449                 $aur_obj->hold(0); 
3450                 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3451             }
3452         }
3453
3454         if($self->api_name =~ /cancel/) {
3455             if ( $cancel_reason ) {
3456                 $aur_obj->cancel_reason( $cancel_reason );
3457                 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3458                 create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
3459             } else {
3460                 $e->delete_acq_user_request($aur_obj);
3461             }
3462         }
3463
3464         $conn->respond({maximum => scalar(@$aur_ids), progress => $x++});
3465     }
3466
3467     $e->commit;
3468     return {complete => 1};
3469 }
3470
3471 __PACKAGE__->register_method (
3472     method    => 'new_user_request',
3473     api_name  => 'open-ils.acq.user_request.create',
3474     signature => {
3475         desc   => 'Create a new user request object in the DB',
3476         param  => [
3477             { desc => 'Authentication token',   type => 'string' },
3478             { desc => 'User request data hash.  Hash keys match the fields for the "aur" object', type => 'object' }
3479         ],
3480         return => {
3481             desc => 'The created user request object, or event on error'
3482         }
3483     }
3484 );
3485
3486 sub new_user_request {
3487     my($self, $conn, $auth, $form_data) = @_;
3488     my $e = new_editor(xact => 1, authtoken => $auth);
3489     return $e->die_event unless $e->checkauth;
3490     my $rid = $e->requestor->id;
3491     my $target_user_fleshed;
3492     if (! defined $$form_data{'usr'}) {
3493         $$form_data{'usr'} = $rid;
3494     }
3495     if ($$form_data{'usr'} != $rid) {
3496         # See if the requestor can place the request on behalf of a different user.
3497         $target_user_fleshed = $e->retrieve_actor_user($$form_data{'usr'}) or return $e->die_event;
3498         $e->allowed('user_request.create', $target_user_fleshed->home_ou) or return $e->die_event;
3499     } else {
3500         $target_user_fleshed = $e->requestor;
3501         $e->allowed('CREATE_PURCHASE_REQUEST') or return $e->die_event;
3502     }
3503     if (! defined $$form_data{'pickup_lib'}) {
3504         if ($target_user_fleshed->ws_ou) {
3505             $$form_data{'pickup_lib'} = $target_user_fleshed->ws_ou;
3506         } else {
3507             $$form_data{'pickup_lib'} = $target_user_fleshed->home_ou;
3508         }
3509     }
3510     if (! defined $$form_data{'request_type'}) {
3511         $$form_data{'request_type'} = 1; # Books
3512     }
3513     my $aur_obj = new Fieldmapper::acq::user_request; 
3514     $aur_obj->isnew(1);
3515     $aur_obj->usr( $$form_data{'usr'} );
3516     $aur_obj->request_date( 'now' );
3517     for my $field ( keys %$form_data ) {
3518         if (defined $$form_data{$field} and $field !~ /^(id|lineitem|eg_bib|request_date|cancel_reason)$/) {
3519             $aur_obj->$field( $$form_data{$field} );
3520         }
3521     }
3522
3523     $aur_obj = $e->create_acq_user_request($aur_obj) or return $e->die_event;
3524
3525     $e->commit and create_user_request_events( $e, [ $aur_obj ], 'aur.created' );
3526
3527     return $aur_obj;
3528 }
3529
3530 sub create_user_request_events {
3531     my($e, $user_reqs, $hook) = @_;
3532
3533     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3534     $ses->connect;
3535
3536     my %cached_usr_home_ou = ();
3537     for my $user_req (@$user_reqs) {
3538         my $home_ou = $cached_usr_home_ou{$user_req->usr};
3539         if (! $home_ou) {
3540             my $user = $e->retrieve_actor_user($user_req->usr) or return $e->die_event;
3541             $home_ou = $user->home_ou;
3542             $cached_usr_home_ou{$user_req->usr} = $home_ou;
3543         }
3544         my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $home_ou);
3545         $req->recv;
3546     }
3547
3548     $ses->disconnect;
3549     return undef;
3550 }
3551
3552
3553 __PACKAGE__->register_method(
3554     method => "po_note_CUD_batch",
3555     api_name => "open-ils.acq.po_note.cud.batch",
3556     stream => 1,
3557     signature => {
3558         desc => q/Manage purchase order notes/,
3559         params => [
3560             {desc => "Authentication token", type => "string"},
3561             {desc => "List of po_notes to manage", type => "array"},
3562         ],
3563         return => {desc => "Stream of successfully managed objects"}
3564     }
3565 );
3566
3567 sub po_note_CUD_batch {
3568     my ($self, $conn, $auth, $notes) = @_;
3569
3570     my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3571     return $e->die_event unless $e->checkauth;
3572     # XXX perms
3573
3574     my $total = @$notes;
3575     my $count = 0;
3576
3577     foreach my $note (@$notes) {
3578
3579         $note->editor($e->requestor->id);
3580         $note->edit_time("now");
3581
3582         if ($note->isnew) {
3583             $note->creator($e->requestor->id);
3584             $note = $e->create_acq_po_note($note) or return $e->die_event;
3585         } elsif ($note->isdeleted) {
3586             $e->delete_acq_po_note($note) or return $e->die_event;
3587         } elsif ($note->ischanged) {
3588             $e->update_acq_po_note($note) or return $e->die_event;
3589         }
3590
3591         unless ($note->isdeleted) {
3592             $note = $e->retrieve_acq_po_note($note->id) or
3593                 return $e->die_event;
3594         }
3595
3596         $conn->respond(
3597             {"maximum" => $total, "progress" => ++$count, "note" => $note}
3598         );
3599     }
3600
3601     $e->commit and $conn->respond_complete or return $e->die_event;
3602 }
3603
3604
3605 # retrieves a lineitem, fleshes its PO and PL, checks perms
3606 # returns ($li, $evt, $org)
3607 sub fetch_and_check_li {
3608     my $e = shift;
3609     my $li_id = shift;
3610     my $perm_mode = shift || 'read';
3611
3612     my $li = $e->retrieve_acq_lineitem([
3613         $li_id,
3614         {   flesh => 1,
3615             flesh_fields => {jub => ['purchase_order', 'picklist']}
3616         }
3617     ]) or return (undef, $e->die_event);
3618
3619     my $org;
3620     if(my $po = $li->purchase_order) {
3621         $org = $po->ordering_agency;
3622         my $perms = ($perm_mode eq 'read') ? 'VIEW_PURCHASE_ORDER' : 'CREATE_PURCHASE_ORDER';
3623         return ($li, $e->die_event) unless $e->allowed($perms, $org);
3624
3625     } elsif(my $pl = $li->picklist) {
3626         $org = $pl->org_unit;
3627         my $perms = ($perm_mode eq 'read') ? 'VIEW_PICKLIST' : 'CREATE_PICKLIST';
3628         return ($li, $e->die_event) unless $e->allowed($perms, $org);
3629     }
3630
3631     return ($li, undef, $org);
3632 }
3633
3634
3635 __PACKAGE__->register_method(
3636     method => "clone_distrib_form",
3637     api_name => "open-ils.acq.distribution_formula.clone",
3638     stream => 1,
3639     signature => {
3640         desc => q/Clone a distribution formula/,
3641         params => [
3642             {desc => "Authentication token", type => "string"},
3643             {desc => "Original formula ID", type => 'integer'},
3644             {desc => "Name of new formula", type => 'string'},
3645         ],
3646         return => {desc => "ID of newly created formula"}
3647     }
3648 );
3649
3650 sub clone_distrib_form {
3651     my($self, $client, $auth, $form_id, $new_name) = @_;
3652
3653     my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3654     return $e->die_event unless $e->checkauth;
3655
3656     my $old_form = $e->retrieve_acq_distribution_formula($form_id) or return $e->die_event;
3657     return $e->die_event unless $e->allowed('ADMIN_ACQ_DISTRIB_FORMULA', $old_form->owner);
3658
3659     my $new_form = Fieldmapper::acq::distribution_formula->new;