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