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