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