]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm
added initial lineitem cancellation api call. added support for creating a/t events...
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Acq / Order.pm
1 package OpenILS::Application::Acq::BatchManager;
2 use OpenSRF::AppSession;
3 use OpenSRF::EX qw/:try/;
4 use strict; use warnings;
5
6 sub new {
7     my($class, %args) = @_;
8     my $self = bless(\%args, $class);
9     $self->{args} = {
10         lid => 0,
11         li => 0,
12         copies => 0,
13         bibs => 0,
14         progress => 0,
15         debits_accrued => 0,
16         purchase_order => undef,
17         picklist => undef,
18         complete => 0,
19         indexed => 0,
20         total => 0
21     };
22     $self->{ingest_queue} = [];
23     $self->{cache} = {};
24     $self->throttle(5) unless $self->throttle;
25     $self->{post_proc_queue} = [];
26     return $self;
27 }
28
29 sub conn {
30     my($self, $val) = @_;
31     $self->{conn} = $val if $val;
32     return $self->{conn};
33 }
34 sub throttle {
35     my($self, $val) = @_;
36     $self->{throttle} = $val if $val;
37     return $self->{throttle};
38 }
39 sub respond {
40     my($self, %other_args) = @_;
41     if($self->throttle and not %other_args) {
42         return unless (
43             ($self->{args}->{progress} - $self->{last_respond_progress}) >= $self->throttle
44         );
45     }
46     $self->conn->respond({ %{$self->{args}}, %other_args });
47     $self->{last_respond_progress} = $self->{args}->{progress};
48 }
49 sub respond_complete {
50     my($self, %other_args) = @_;
51     $self->complete;
52     $self->conn->respond_complete({ %{$self->{args}}, %other_args });
53     $self->run_post_response_hooks;
54     return undef;
55 }
56
57 # run the post response hook subs, shifting them off as we go
58 sub run_post_response_hooks {
59     my($self) = @_;
60     (shift @{$self->{post_proc_queue}})->() while @{$self->{post_proc_queue}};
61 }
62
63 # any subs passed to this method will be run after the call to respond_complete
64 sub post_process {
65     my($self, $sub) = @_;
66     push(@{$self->{post_proc_queue}}, $sub);
67 }
68
69 sub total {
70     my($self, $val) = @_;
71     $self->{args}->{total} = $val if defined $val;
72     $self->{args}->{maximum} = $self->{args}->{total};
73     return $self->{args}->{total};
74 }
75 sub purchase_order {
76     my($self, $val) = @_;
77     $self->{args}->{purchase_order} = $val if $val;
78     return $self;
79 }
80 sub picklist {
81     my($self, $val) = @_;
82     $self->{args}->{picklist} = $val if $val;
83     return $self;
84 }
85 sub add_lid {
86     my $self = shift;
87     $self->{args}->{lid} += 1;
88     $self->{args}->{progress} += 1;
89     return $self;
90 }
91 sub add_li {
92     my $self = shift;
93     $self->{args}->{li} += 1;
94     $self->{args}->{progress} += 1;
95     return $self;
96 }
97 sub add_copy {
98     my $self = shift;
99     $self->{args}->{copies} += 1;
100     $self->{args}->{progress} += 1;
101     return $self;
102 }
103 sub add_bib {
104     my $self = shift;
105     $self->{args}->{bibs} += 1;
106     $self->{args}->{progress} += 1;
107     return $self;
108 }
109 sub add_debit {
110     my($self, $amount) = @_;
111     $self->{args}->{debits_accrued} += $amount;
112     $self->{args}->{progress} += 1;
113     return $self;
114 }
115 sub editor {
116     my($self, $editor) = @_;
117     $self->{editor} = $editor if defined $editor;
118     return $self->{editor};
119 }
120 sub complete {
121     my $self = shift;
122     $self->{args}->{complete} = 1;
123     return $self;
124 }
125
126 sub ingest_ses {
127     my($self, $val) = @_;
128     $self->{ingest_ses} = $val if $val;
129     return $self->{ingest_ses};
130 }
131
132 sub push_ingest_queue {
133     my($self, $rec_id) = @_;
134
135     $self->ingest_ses(OpenSRF::AppSession->connect('open-ils.ingest'))
136         unless $self->ingest_ses;
137
138     my $req = $self->ingest_ses->request('open-ils.ingest.full.biblio.record', $rec_id);
139
140     push(@{$self->{ingest_queue}}, $req);
141 }
142
143 sub process_ingest_records {
144     my $self = shift;
145     return unless @{$self->{ingest_queue}};
146
147     for my $req (@{$self->{ingest_queue}}) {
148
149         try { 
150             $req->gather(1); 
151             $self->{args}->{indexed} += 1;
152             $self->{args}->{progress} += 1;
153         } otherwise {};
154
155         $self->respond;
156     }
157     $self->ingest_ses->disconnect;
158 }
159
160
161 sub cache {
162     my($self, $org, $key, $val) = @_;
163     $self->{cache}->{$org} = {} unless $self->{cache}->{org};
164     $self->{cache}->{$org}->{$key} = $val if defined $val;
165     return $self->{cache}->{$org}->{$key};
166 }
167
168
169 package OpenILS::Application::Acq::Order;
170 use base qw/OpenILS::Application/;
171 use strict; use warnings;
172 # ----------------------------------------------------------------------------
173 # Break up each component of the order process and pieces into managable
174 # actions that can be shared across different workflows
175 # ----------------------------------------------------------------------------
176 use OpenILS::Event;
177 use OpenSRF::Utils::Logger qw(:logger);
178 use OpenSRF::Utils::JSON;
179 use OpenSRF::AppSession;
180 use OpenILS::Utils::Fieldmapper;
181 use OpenILS::Utils::CStoreEditor q/:funcs/;
182 use OpenILS::Const qw/:const/;
183 use OpenSRF::EX q/:try/;
184 use OpenILS::Application::AppUtils;
185 use OpenILS::Application::Cat::BibCommon;
186 use OpenILS::Application::Cat::AssetCommon;
187 use MARC::Record;
188 use MARC::Batch;
189 use MARC::File::XML;
190 my $U = 'OpenILS::Application::AppUtils';
191
192
193 # ----------------------------------------------------------------------------
194 # Lineitem
195 # ----------------------------------------------------------------------------
196 sub create_lineitem {
197     my($mgr, %args) = @_;
198     my $li = Fieldmapper::acq::lineitem->new;
199     $li->creator($mgr->editor->requestor->id);
200     $li->selector($li->creator);
201     $li->editor($li->creator);
202     $li->create_time('now');
203     $li->edit_time('now');
204     $li->state('new');
205     $li->$_($args{$_}) for keys %args;
206     $li->clear_id;
207     $mgr->add_li;
208     return $mgr->editor->create_acq_lineitem($li);
209 }
210
211 sub update_lineitem {
212     my($mgr, $li) = @_;
213     $li->edit_time('now');
214     $li->editor($mgr->editor->requestor->id);
215     $mgr->add_li;
216     return $li if $mgr->editor->update_acq_lineitem($li); 
217     return undef;
218 }
219
220
221 # ----------------------------------------------------------------------------
222 # Create real holds from patron requests for a given lineitem
223 # ----------------------------------------------------------------------------
224 sub promote_lineitem_holds {
225     my($mgr, $li) = @_;
226
227     my $requests = $mgr->editor->search_acq_user_request(
228         { lineitem => $li->id,
229           '-or' =>
230             [ { need_before => {'>' => 'now'} },
231               { need_before => undef }
232             ]
233         }
234     );
235
236     for my $request ( @$requests ) {
237
238         $request->eg_bib( $li->eg_bib_id );
239         $mgr->editor->update_acq_user_request( $request ) or return 0;
240
241         next unless ($U->is_true( $request->hold ));
242
243         my $hold = Fieldmapper::action::hold_request->new;
244         $hold->usr( $request->usr );
245         $hold->requestor( $request->usr );
246         $hold->request_time( $request->request_date );
247         $hold->pickup_lib( $request->pickup_lib );
248         $hold->request_lib( $request->pickup_lib );
249         $hold->selection_ou( $request->pickup_lib );
250         $hold->phone_notify( $request->phone_notify );
251         $hold->email_notify( $request->email_notify );
252         $hold->expire_time( $request->need_before );
253
254         if ($request->holdable_formats) {
255             my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
256             if ($mrm) {
257                 $hold->hold_type( 'M' );
258                 $hold->holdable_formats( $request->holdable_formats );
259                 $hold->target( $mrm->metarecord );
260             }
261         }
262
263         if (!$hold->target) {
264             $hold->hold_type( 'T' );
265             $hold->target( $li->eg_bib_id );
266         }
267
268         $mgr->editor->create_actor_hold_request( $hold ) or return 0;
269     }
270
271     return $li;
272 }
273
274 sub delete_lineitem {
275     my($mgr, $li) = @_;
276     $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
277
278     # delete the attached lineitem_details
279     my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
280     for my $lid_id (@$lid_ids) {
281         return 0 unless delete_lineitem_detail($mgr, $lid_id);
282     }
283
284     $mgr->add_li;
285     return $mgr->editor->delete_acq_lineitem($li);
286 }
287
288 # begins and commit transactions as it goes
289 sub create_lineitem_list_assets {
290     my($mgr, $li_ids) = @_;
291     return undef if check_import_li_marc_perms($mgr, $li_ids);
292
293     # create the bibs/volumes/copies and ingest the records
294     for my $li_id (@$li_ids) {
295         $mgr->editor->xact_begin;
296         my $data = create_lineitem_assets($mgr, $li_id) or return undef;
297         $mgr->editor->xact_commit;
298         # XXX ingest is in-db now
299         #$mgr->push_ingest_queue($data->{li}->eg_bib_id) if $data->{new_bib};
300         $mgr->respond;
301     }
302     $mgr->process_ingest_records;
303     return 1;
304 }
305
306 # returns event on error, undef on success
307 sub check_import_li_marc_perms {
308     my($mgr, $li_ids) = @_;
309
310     # if there are any order records that are not linked to 
311     # in-db bib records, verify staff has perms to import order records
312     my $order_li = $mgr->editor->search_acq_lineitem(
313         [{id => $li_ids, eg_bib_id => undef}, {limit => 1}], {idlist => 1})->[0];
314
315     if($order_li) {
316         return $mgr->editor->die_event unless 
317             $mgr->editor->allowed('IMPORT_ACQ_LINEITEM_BIB_RECORD');
318     }
319
320     return undef;
321 }
322
323
324 # ----------------------------------------------------------------------------
325 # if all of the lineitem details for this lineitem have 
326 # been received, mark the lineitem as received
327 # returns 1 on non-received, li on received, 0 on error
328 # ----------------------------------------------------------------------------
329 sub check_lineitem_received {
330     my($mgr, $li_id) = @_;
331
332     my $non_recv = $mgr->editor->search_acq_lineitem_detail(
333         {recv_time => undef, lineitem => $li_id}, {idlist=>1});
334
335     return 1 if @$non_recv;
336
337     my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
338     $li->state('received');
339     return update_lineitem($mgr, $li);
340 }
341
342 sub receive_lineitem {
343     my($mgr, $li_id, $skip_complete_check) = @_;
344     my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
345
346     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
347         {lineitem => $li_id, recv_time => undef}, {idlist => 1});
348
349     for my $lid_id (@$lid_ids) {
350        receive_lineitem_detail($mgr, $lid_id, 1) or return 0; 
351     }
352
353     $mgr->add_li;
354     $li->state('received');
355     update_lineitem($mgr, $li) or return 0;
356
357     $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
358
359     return 1 if $skip_complete_check;
360
361     return check_purchase_order_received($mgr, $li->purchase_order);
362 }
363
364 sub rollback_receive_lineitem {
365     my($mgr, $li_id) = @_;
366     my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
367
368     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
369         {lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
370
371     for my $lid_id (@$lid_ids) {
372        rollback_receive_lineitem_detail($mgr, $lid_id, 1) or return 0; 
373     }
374
375     $mgr->add_li;
376     $li->state('on-order');
377     return update_lineitem($mgr, $li);
378 }
379
380
381 sub create_lineitem_status_events {
382     my($mgr, $li_id, $hook) = @_;
383
384     my $ses = OpenSRF::AppSession->create('open-ils.trigger');
385     $ses->connect;
386     my $user_reqs = $mgr->editor->search_acq_user_request([
387         {lineitem => $li_id}, 
388         {flesh => 1, flesh_fields => {aur => ['usr']}}
389     ]);
390
391     for my $user_req (@$user_reqs) {
392         my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $user_req->usr->home_ou);
393         $req->recv; 
394     }
395
396     $ses->disconnect;
397     return undef;
398 }
399
400 # ----------------------------------------------------------------------------
401 # Lineitem Detail
402 # ----------------------------------------------------------------------------
403 sub create_lineitem_detail {
404     my($mgr, %args) = @_;
405     my $lid = Fieldmapper::acq::lineitem_detail->new;
406     $lid->$_($args{$_}) for keys %args;
407     $lid->clear_id;
408     $mgr->add_lid;
409     return $mgr->editor->create_acq_lineitem_detail($lid);
410 }
411
412
413 # flesh out any required data with default values where appropriate
414 sub complete_lineitem_detail {
415     my($mgr, $lid) = @_;
416     unless($lid->barcode) {
417         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
418         $lid->barcode($pfx.$lid->id);
419     }
420
421     unless($lid->cn_label) {
422         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
423         $lid->cn_label($pfx.$lid->id);
424     }
425
426     if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
427         $lid->location($loc);
428     }
429
430     if(!$lid->circ_modifier and my $mod = get_default_circ_modifier($mgr, $lid->owning_lib)) {
431         $lid->circ_modifier($mod);
432     }
433
434     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
435     return $lid;
436 }
437
438 sub get_default_circ_modifier {
439     my($mgr, $org) = @_;
440     my $mod = $mgr->cache($org, 'def_circ_mod');
441     return $mod if $mod;
442     $mod = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier');
443     return $mgr->cache($org, 'def_circ_mod', $mod) if $mod;
444     return undef;
445 }
446
447 sub delete_lineitem_detail {
448     my($mgr, $lid) = @_;
449     $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
450     return $mgr->editor->delete_acq_lineitem_detail($lid);
451 }
452
453
454 sub receive_lineitem_detail {
455     my($mgr, $lid_id, $skip_complete_check) = @_;
456     my $e = $mgr->editor;
457
458     my $lid = $e->retrieve_acq_lineitem_detail([
459         $lid_id,
460         {   flesh => 1,
461             flesh_fields => {
462                 acqlid => ['fund_debit']
463             }
464         }
465     ]) or return 0;
466
467     return 1 if $lid->recv_time;
468
469     $lid->recv_time('now');
470     $e->update_acq_lineitem_detail($lid) or return 0;
471
472     my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
473     $copy->status(OILS_COPY_STATUS_IN_PROCESS);
474     $copy->edit_date('now');
475     $copy->editor($e->requestor->id);
476     $e->update_asset_copy($copy) or return 0;
477
478     if($lid->fund_debit) {
479         $lid->fund_debit->encumbrance('f');
480         $e->update_acq_fund_debit($lid->fund_debit) or return 0;
481     }
482
483     $mgr->add_lid;
484
485     return 1 if $skip_complete_check;
486
487     my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
488     return 1 if $li == 1; # li not received
489
490     return check_purchase_order_received($mgr, $li->purchase_order);
491 }
492
493
494 sub rollback_receive_lineitem_detail {
495     my($mgr, $lid_id) = @_;
496     my $e = $mgr->editor;
497
498     my $lid = $e->retrieve_acq_lineitem_detail([
499         $lid_id,
500         {   flesh => 1,
501             flesh_fields => {
502                 acqlid => ['fund_debit']
503             }
504         }
505     ]) or return 0;
506
507     return 1 unless $lid->recv_time;
508
509     $lid->clear_recv_time;
510     $e->update_acq_lineitem_detail($lid) or return 0;
511
512     my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
513     $copy->status(OILS_COPY_STATUS_ON_ORDER);
514     $copy->edit_date('now');
515     $copy->editor($e->requestor->id);
516     $e->update_asset_copy($copy) or return 0;
517
518     if($lid->fund_debit) {
519         $lid->fund_debit->encumbrance('t');
520         $e->update_acq_fund_debit($lid->fund_debit) or return 0;
521     }
522
523     $mgr->add_lid;
524     return $lid;
525 }
526
527 # ----------------------------------------------------------------------------
528 # Lineitem Attr
529 # ----------------------------------------------------------------------------
530 sub set_lineitem_attr {
531     my($mgr, %args) = @_;
532     my $attr_type = $args{attr_type};
533
534     # first, see if it's already set.  May just need to overwrite it
535     my $attr = $mgr->editor->search_acq_lineitem_attr({
536         lineitem => $args{lineitem},
537         attr_type => $args{attr_type},
538         attr_name => $args{attr_name}
539     })->[0];
540
541     if($attr) {
542         $attr->attr_value($args{attr_value});
543         return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
544         return undef;
545
546     } else {
547
548         $attr = Fieldmapper::acq::lineitem_attr->new;
549         $attr->$_($args{$_}) for keys %args;
550         
551         unless($attr->definition) {
552             my $find = "search_acq_$attr_type";
553             my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
554             $attr->definition($attr_def_id);
555         }
556         return $mgr->editor->create_acq_lineitem_attr($attr);
557     }
558 }
559
560 sub get_li_price {
561     my $li = shift;
562     my $attrs = $li->attributes;
563     my ($marc_estimated, $local_estimated, $local_actual, $prov_estimated, $prov_actual);
564
565     for my $attr (@$attrs) {
566         if($attr->attr_name eq 'estimated_price') {
567             $local_estimated = $attr->attr_value 
568                 if $attr->attr_type eq 'lineitem_local_attr_definition';
569             $prov_estimated = $attr->attr_value 
570                 if $attr->attr_type eq 'lineitem_prov_attr_definition';
571             $marc_estimated = $attr->attr_value
572                 if $attr->attr_type eq 'lineitem_marc_attr_definition';
573
574         } elsif($attr->attr_name eq 'actual_price') {
575             $local_actual = $attr->attr_value     
576                 if $attr->attr_type eq 'lineitem_local_attr_definition';
577             $prov_actual = $attr->attr_value 
578                 if $attr->attr_type eq 'lineitem_prov_attr_definition';
579         }
580     }
581
582     return ($local_actual, 1) if $local_actual;
583     return ($prov_actual, 2) if $prov_actual;
584     return ($local_estimated, 1) if $local_estimated;
585     return ($prov_estimated, 2) if $prov_estimated;
586     return ($marc_estimated, 3);
587 }
588
589
590 # ----------------------------------------------------------------------------
591 # Lineitem Debits
592 # ----------------------------------------------------------------------------
593 sub create_lineitem_debits {
594     my($mgr, $li, $price, $ptype) = @_; 
595
596     ($price, $ptype) = get_li_price($li) unless $price;
597
598     unless($price) {
599         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
600         $mgr->editor->rollback;
601         return 0;
602     }
603
604     unless($li->provider) {
605         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
606         $mgr->editor->rollback;
607         return 0;
608     }
609
610     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
611         {lineitem => $li->id}, 
612         {idlist=>1}
613     );
614
615     for my $lid_id (@$lid_ids) {
616
617         my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
618             $lid_id,
619             {   flesh => 1, 
620                 flesh_fields => {acqlid => ['fund']}
621             }
622         ]);
623
624         create_lineitem_detail_debit($mgr, $li, $lid, $price, $ptype) or return 0;
625     }
626
627     return 1;
628 }
629
630
631 # flesh li->provider
632 # flesh lid->fund
633 # ptype 1=local, 2=provider, 3=marc
634 sub create_lineitem_detail_debit {
635     my($mgr, $li, $lid, $price, $ptype) = @_;
636
637     unless(ref $li and ref $li->provider) {
638        $li = $mgr->editor->retrieve_acq_lineitem([
639             $li,
640             {   flesh => 1,
641                 flesh_fields => {jub => ['provider']},
642             }
643         ]);
644     }
645
646     unless(ref $lid and ref $lid->fund) {
647         $lid = $mgr->editor->retrieve_acq_lineitem_detail([
648             $lid,
649             {   flesh => 1, 
650                 flesh_fields => {acqlid => ['fund']}
651             }
652         ]);
653     }
654
655     my $ctype = $lid->fund->currency_type;
656     my $amount = $price;
657
658     if($ptype == 2) { # price from vendor
659         $ctype = $li->provider->currency_type;
660         $amount = currency_conversion($mgr, $ctype, $lid->fund->currency_type, $price);
661     }
662
663     my $debit = create_fund_debit(
664         $mgr, 
665         fund => $lid->fund->id,
666         origin_amount => $price,
667         origin_currency_type => $ctype,
668         amount => $amount
669     ) or return 0;
670
671     $lid->fund_debit($debit->id);
672     $lid->fund($lid->fund->id);
673     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
674     return $debit;
675 }
676
677
678 # ----------------------------------------------------------------------------
679 # Fund Debit
680 # ----------------------------------------------------------------------------
681 sub create_fund_debit {
682     my($mgr, %args) = @_;
683
684     # Verify the fund is not being spent beyond the hard stop amount
685     my $fund = $mgr->editor->retrieve_acq_fund($args{fund}) or return 0;
686
687     if($fund->balance_stop_percent) {
688
689         my $balance = $mgr->editor->search_acq_fund_combined_balance({fund => $fund->id})->[0];
690         my $allocations = $mgr->editor->search_acq_fund_allocation_total({fund => $fund->id})->[0];
691         $balance = ($balance) ? $balance->amount : 0;
692         $allocations = ($allocations) ? $allocations->amount : 0;
693
694         if( 
695             $allocations == 0 || # if no allocations were ever made, assume we have hit the stop percent
696             ( ( ( ($balance - $args{amount}) / $allocations ) * 100 ) < $fund->balance_stop_percent)) 
697         {
698                 $mgr->editor->event(OpenILS::Event->new(
699                     'FUND_EXCEEDS_STOP_PERCENT', 
700                     payload => {fund => $fund->id, debit_amount => $args{amount}}
701                 ));
702                 return 0;
703         }
704     }
705
706     my $debit = Fieldmapper::acq::fund_debit->new;
707     $debit->debit_type('purchase');
708     $debit->encumbrance('t');
709     $debit->$_($args{$_}) for keys %args;
710     $debit->clear_id;
711     $mgr->add_debit($debit->amount);
712     return $mgr->editor->create_acq_fund_debit($debit);
713 }
714
715 sub currency_conversion {
716     my($mgr, $src_currency, $dest_currency, $amount) = @_;
717     my $result = $mgr->editor->json_query(
718         {from => ['acq.exchange_ratio', $src_currency, $dest_currency, $amount]});
719     return $result->[0]->{'acq.exchange_ratio'};
720 }
721
722
723 # ----------------------------------------------------------------------------
724 # Picklist
725 # ----------------------------------------------------------------------------
726 sub create_picklist {
727     my($mgr, %args) = @_;
728     my $picklist = Fieldmapper::acq::picklist->new;
729     $picklist->creator($mgr->editor->requestor->id);
730     $picklist->owner($picklist->creator);
731     $picklist->editor($picklist->creator);
732     $picklist->create_time('now');
733     $picklist->edit_time('now');
734     $picklist->org_unit($mgr->editor->requestor->ws_ou);
735     $picklist->owner($mgr->editor->requestor->id);
736     $picklist->$_($args{$_}) for keys %args;
737     $picklist->clear_id;
738     $mgr->picklist($picklist);
739     return $mgr->editor->create_acq_picklist($picklist);
740 }
741
742 sub update_picklist {
743     my($mgr, $picklist) = @_;
744     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
745     $picklist->edit_time('now');
746     $picklist->editor($mgr->editor->requestor->id);
747     $mgr->picklist($picklist);
748     return $picklist if $mgr->editor->update_acq_picklist($picklist);
749     return undef;
750 }
751
752 sub delete_picklist {
753     my($mgr, $picklist) = @_;
754     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
755
756     # delete all 'new' lineitems
757     my $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => 'new'}, {idlist => 1});
758     for my $li_id (@$li_ids) {
759         my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
760         return 0 unless delete_lineitem($mgr, $li);
761         $mgr->respond;
762     }
763
764     # detach all non-'new' lineitems
765     $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}}, {idlist => 1});
766     for my $li_id (@$li_ids) {
767         my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
768         $li->clear_picklist;
769         return 0 unless update_lineitem($mgr, $li);
770         $mgr->respond;
771     }
772
773     # remove any picklist-specific object perms
774     my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
775     for my $op (@$ops) {
776         return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
777     }
778
779     return $mgr->editor->delete_acq_picklist($picklist);
780 }
781
782 # ----------------------------------------------------------------------------
783 # Purchase Order
784 # ----------------------------------------------------------------------------
785 sub update_purchase_order {
786     my($mgr, $po) = @_;
787     $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
788     $po->editor($mgr->editor->requestor->id);
789     $po->edit_time('now');
790     $mgr->purchase_order($po);
791     return $po if $mgr->editor->update_acq_purchase_order($po);
792     return undef;
793 }
794
795 sub create_purchase_order {
796     my($mgr, %args) = @_;
797
798     # verify the chosen provider is still active
799     my $provider = $mgr->editor->retrieve_acq_provider($args{provider}) or return 0;
800     unless($U->is_true($provider->active)) {
801         $logger->error("provider is not active.  cannot create PO");
802         $mgr->editor->event(OpenILS::Event->new('ACQ_PROVIDER_INACTIVE'));
803         return 0;
804     }
805
806     my $po = Fieldmapper::acq::purchase_order->new;
807     $po->creator($mgr->editor->requestor->id);
808     $po->editor($mgr->editor->requestor->id);
809     $po->owner($mgr->editor->requestor->id);
810     $po->edit_time('now');
811     $po->create_time('now');
812     $po->state('pending');
813     $po->ordering_agency($mgr->editor->requestor->ws_ou);
814     $po->$_($args{$_}) for keys %args;
815     $po->clear_id;
816     $mgr->purchase_order($po);
817     return $mgr->editor->create_acq_purchase_order($po);
818 }
819
820 # ----------------------------------------------------------------------------
821 # if all of the lineitems for this PO are received,
822 # mark the PO as received
823 # ----------------------------------------------------------------------------
824 sub check_purchase_order_received {
825     my($mgr, $po_id) = @_;
826
827     my $non_recv_li = $mgr->editor->search_acq_lineitem(
828         {   purchase_order => $po_id,
829             state => {'!=' => 'received'}
830         }, {idlist=>1});
831
832     return 1 if @$non_recv_li;
833
834     my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
835     $po->state('received');
836     return update_purchase_order($mgr, $po);
837 }
838
839
840 # ----------------------------------------------------------------------------
841 # Bib, Callnumber, and Copy data
842 # ----------------------------------------------------------------------------
843
844 sub create_lineitem_assets {
845     my($mgr, $li_id) = @_;
846     my $evt;
847
848     my $li = $mgr->editor->retrieve_acq_lineitem([
849         $li_id,
850         {   flesh => 1,
851             flesh_fields => {jub => ['purchase_order', 'attributes']}
852         }
853     ]) or return 0;
854
855     # -----------------------------------------------------------------
856     # first, create the bib record if necessary
857     # -----------------------------------------------------------------
858     my $new_bib = 0;
859     unless($li->eg_bib_id) {
860         create_bib($mgr, $li) or return 0;
861         $new_bib = 1;
862     }
863
864
865     # -----------------------------------------------------------------
866     # The lineitem is going live, promote user request holds to real holds
867     # -----------------------------------------------------------------
868     promote_lineitem_holds($mgr, $li) or return 0;
869
870     my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
871
872     # -----------------------------------------------------------------
873     # for each lineitem_detail, create the volume if necessary, create 
874     # a copy, and link them all together.
875     # -----------------------------------------------------------------
876     my $first_cn;
877     for my $lid_id (@{$li_details}) {
878
879         my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
880         next if $lid->eg_copy_id;
881
882         # use the same callnumber label for all items within this lineitem
883         $lid->cn_label($first_cn) if $first_cn and not $lid->cn_label;
884
885         # apply defaults if necessary
886         return 0 unless complete_lineitem_detail($mgr, $lid);
887
888         $first_cn = $lid->cn_label unless $first_cn;
889
890         my $org = $lid->owning_lib;
891         my $label = $lid->cn_label;
892         my $bibid = $li->eg_bib_id;
893
894         my $volume = $mgr->cache($org, "cn.$bibid.$label");
895         unless($volume) {
896             $volume = create_volume($mgr, $li, $lid) or return 0;
897             $mgr->cache($org, "cn.$bibid.$label", $volume);
898         }
899         create_copy($mgr, $volume, $lid) or return 0;
900     }
901
902     return { li => $li, new_bib => $new_bib };
903 }
904
905 sub create_bib {
906     my($mgr, $li) = @_;
907
908     my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
909         $mgr->editor, 
910         $li->marc, 
911         undef, # bib source
912         undef, 
913         1, # override tcn collisions
914     ); 
915
916     if($U->event_code($record)) {
917         $mgr->editor->event($record);
918         $mgr->editor->rollback;
919         return 0;
920     }
921
922     $li->eg_bib_id($record->id);
923     $mgr->add_bib;
924     return update_lineitem($mgr, $li);
925 }
926
927 sub create_volume {
928     my($mgr, $li, $lid) = @_;
929
930     my ($volume, $evt) = 
931         OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
932             $mgr->editor, 
933             $lid->cn_label, 
934             $li->eg_bib_id, 
935             $lid->owning_lib
936         );
937
938     if($evt) {
939         $mgr->editor->event($evt);
940         return 0;
941     }
942
943     return $volume;
944 }
945
946 sub create_copy {
947     my($mgr, $volume, $lid) = @_;
948     my $copy = Fieldmapper::asset::copy->new;
949     $copy->isnew(1);
950     $copy->loan_duration(2);
951     $copy->fine_level(2);
952     $copy->status(OILS_COPY_STATUS_ON_ORDER);
953     $copy->barcode($lid->barcode);
954     $copy->location($lid->location);
955     $copy->call_number($volume->id);
956     $copy->circ_lib($volume->owning_lib);
957     $copy->circ_modifier($lid->circ_modifier);
958
959     my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
960     if($evt) {
961         $mgr->editor->event($evt);
962         return 0;
963     }
964
965     $mgr->add_copy;
966     $lid->eg_copy_id($copy->id);
967     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
968 }
969
970
971
972
973
974
975 # ----------------------------------------------------------------------------
976 # Workflow: Build a selection list from a Z39.50 search
977 # ----------------------------------------------------------------------------
978
979 __PACKAGE__->register_method(
980         method => 'zsearch',
981         api_name => 'open-ils.acq.picklist.search.z3950',
982     stream => 1,
983         signature => {
984         desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
985         params => [
986             {desc => 'Authentication token', type => 'string'},
987             {desc => 'Search definition', type => 'object'},
988             {desc => 'Picklist name, optional', type => 'string'},
989         ]
990     }
991 );
992
993 sub zsearch {
994     my($self, $conn, $auth, $search, $name, $options) = @_;
995     my $e = new_editor(authtoken=>$auth);
996     return $e->event unless $e->checkauth;
997     return $e->event unless $e->allowed('CREATE_PICKLIST');
998
999     $search->{limit} ||= 10;
1000     $options ||= {};
1001
1002     my $ses = OpenSRF::AppSession->create('open-ils.search');
1003     my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
1004
1005     my $first = 1;
1006     my $picklist;
1007     my $mgr;
1008     while(my $resp = $req->recv(timeout=>60)) {
1009
1010         if($first) {
1011             my $e = new_editor(requestor=>$e->requestor, xact=>1);
1012             $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1013             $picklist = zsearch_build_pl($mgr, $name);
1014             $first = 0;
1015         }
1016
1017         my $result = $resp->content;
1018         my $count = $result->{count};
1019         $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
1020
1021         for my $rec (@{$result->{records}}) {
1022
1023             my $li = create_lineitem($mgr, 
1024                 picklist => $picklist->id,
1025                 source_label => $result->{service},
1026                 marc => $rec->{marcxml},
1027                 eg_bib_id => $rec->{bibid}
1028             );
1029
1030             if($$options{respond_li}) {
1031                 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
1032                     if $$options{flesh_attrs};
1033                 $li->clear_marc if $$options{clear_marc};
1034                 $mgr->respond(lineitem => $li);
1035             } else {
1036                 $mgr->respond;
1037             }
1038         }
1039     }
1040
1041     $mgr->editor->commit;
1042     return $mgr->respond_complete;
1043 }
1044
1045 sub zsearch_build_pl {
1046     my($mgr, $name) = @_;
1047     $name ||= '';
1048
1049     my $picklist = $mgr->editor->search_acq_picklist({
1050         owner => $mgr->editor->requestor->id, 
1051         name => $name
1052     })->[0];
1053
1054     if($name eq '' and $picklist) {
1055         return 0 unless delete_picklist($mgr, $picklist);
1056         $picklist = undef;
1057     }
1058
1059     return update_picklist($mgr, $picklist) if $picklist;
1060     return create_picklist($mgr, name => $name);
1061 }
1062
1063
1064 # ----------------------------------------------------------------------------
1065 # Workflow: Build a selection list / PO by importing a batch of MARC records
1066 # ----------------------------------------------------------------------------
1067
1068 __PACKAGE__->register_method(
1069     method => 'upload_records',
1070     api_name => 'open-ils.acq.process_upload_records',
1071     stream => 1,
1072 );
1073
1074 sub upload_records {
1075     my($self, $conn, $auth, $key) = @_;
1076
1077         my $e = new_editor(authtoken => $auth, xact => 1);
1078     return $e->die_event unless $e->checkauth;
1079     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1080
1081     my $cache = OpenSRF::Utils::Cache->new;
1082
1083     my $data = $cache->get_cache("vandelay_import_spool_$key");
1084         my $purpose = $data->{purpose};
1085     my $filename = $data->{path};
1086     my $provider = $data->{provider};
1087     my $picklist = $data->{picklist};
1088     my $create_po = $data->{create_po};
1089     my $ordering_agency = $data->{ordering_agency};
1090     my $create_assets = $data->{create_assets};
1091     my $po;
1092     my $evt;
1093
1094     unless(-r $filename) {
1095         $logger->error("unable to read MARC file $filename");
1096         $e->rollback;
1097         return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
1098     }
1099
1100     $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
1101
1102     if($picklist) {
1103         $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
1104         if($picklist->owner != $e->requestor->id) {
1105             return $e->die_event unless 
1106                 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
1107         }
1108         $mgr->picklist($picklist);
1109     }
1110
1111     if($create_po) {
1112
1113         $po = create_purchase_order($mgr, 
1114             ordering_agency => $ordering_agency,
1115             provider => $provider->id,
1116             state => 'on-order'
1117         ) or return $mgr->editor->die_event;
1118     }
1119
1120     $logger->info("acq processing MARC file=$filename");
1121
1122     my $marctype = 'USMARC'; # ?
1123         my $batch = new MARC::Batch ($marctype, $filename);
1124         $batch->strict_off;
1125
1126         my $count = 0;
1127     my @li_list;
1128
1129         while(1) {
1130
1131             my $err;
1132         my $xml;
1133                 $count++;
1134         my $r;
1135
1136                 try {
1137             $r = $batch->next;
1138         } catch Error with {
1139             $err = shift;
1140                         $logger->warn("Proccessing of record $count in set $key failed with error $err.  Skipping this record");
1141         };
1142
1143         next if $err;
1144         last unless $r;
1145
1146                 try {
1147             ($xml = $r->as_xml_record()) =~ s/\n//sog;
1148             $xml =~ s/^<\?xml.+\?\s*>//go;
1149             $xml =~ s/>\s+</></go;
1150             $xml =~ s/\p{Cc}//go;
1151             $xml = $U->entityize($xml);
1152             $xml =~ s/[\x00-\x1f]//go;
1153
1154                 } catch Error with {
1155                         $err = shift;
1156                         $logger->warn("Proccessing XML of record $count in set $key failed with error $err.  Skipping this record");
1157                 };
1158
1159         next if $err or not $xml;
1160
1161         my %args = (
1162             source_label => $provider->code,
1163             provider => $provider->id,
1164             marc => $xml,
1165         );
1166
1167         $args{picklist} = $picklist->id if $picklist;
1168         if($po) {
1169             $args{purchase_order} = $po->id;
1170             $args{state} = 'on-order';
1171         }
1172
1173         my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
1174         $mgr->respond;
1175         $li->provider($provider); # flesh it, we'll need it later
1176
1177         import_lineitem_details($mgr, $ordering_agency, $li) or return $mgr->editor->die_event;
1178         $mgr->respond;
1179
1180         push(@li_list, $li->id);
1181         $mgr->respond;
1182         }
1183
1184         $e->commit;
1185     unlink($filename);
1186     $cache->delete_cache('vandelay_import_spool_' . $key);
1187
1188     if($create_assets) {
1189         create_lineitem_list_assets($mgr, \@li_list) or return $e->die_event;
1190     }
1191
1192     return $mgr->respond_complete;
1193 }
1194
1195 sub import_lineitem_details {
1196     my($mgr, $ordering_agency, $li) = @_;
1197
1198     my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
1199     return 1 unless @$holdings;
1200     my $org_path = $U->get_org_ancestors($ordering_agency);
1201     $org_path = [ reverse (@$org_path) ];
1202     my $price;
1203
1204     my $idx = 1;
1205     while(1) {
1206         # create a lineitem detail for each copy in the data
1207
1208         my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx);
1209         last unless defined $compiled;
1210         return 0 unless $compiled;
1211
1212         # this takes the price of the last copy and uses it as the lineitem price
1213         # need to determine if a given record would include different prices for the same item
1214         $price = $$compiled{price};
1215
1216         for(1..$$compiled{quantity}) {
1217             my $lid = create_lineitem_detail($mgr, 
1218                 lineitem => $li->id,
1219                 owning_lib => $$compiled{owning_lib},
1220                 cn_label => $$compiled{call_number},
1221                 fund => $$compiled{fund},
1222                 circ_modifier => $$compiled{circ_modifier},
1223                 note => $$compiled{note},
1224                 location => $$compiled{copy_location},
1225                 collection_code => $$compiled{collection_code}
1226             ) or return 0;
1227         }
1228
1229         $mgr->respond;
1230         $idx++;
1231     }
1232
1233     # set the price attr so we'll know the source of the price
1234     set_lineitem_attr(
1235         $mgr, 
1236         attr_name => 'estimated_price',
1237         attr_type => 'lineitem_local_attr_definition',
1238         attr_value => $price,
1239         lineitem => $li->id
1240     ) or return 0;
1241
1242     # if we're creating a purchase order, create the debits
1243     if($li->purchase_order) {
1244         create_lineitem_debits($mgr, $li, $price, 2) or return 0;
1245         $mgr->respond;
1246     }
1247
1248     return 1;
1249 }
1250
1251 # return hash on success, 0 on error, undef on no more holdings
1252 sub extract_lineitem_detail_data {
1253     my($mgr, $org_path, $holdings, $index) = @_;
1254
1255     my @data_list = grep { $_->{holding} eq $index } @$holdings;
1256     return undef unless @data_list;
1257
1258     my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1259     my $base_org = $$org_path[0];
1260
1261     my $killme = sub {
1262         my $msg = shift;
1263         $logger->error("Item import extraction error: $msg");
1264         $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1265         $mgr->editor->rollback;
1266         $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1267         return 0;
1268     };
1269
1270     $compiled{quantity} ||= 1;
1271
1272     # ---------------------------------------------------------------------
1273     # Fund
1274     my $code = $compiled{fund_code};
1275     return $killme->('no fund code provided') unless $code;
1276
1277     my $fund = $mgr->cache($base_org, "fund.$code");
1278     unless($fund) {
1279         # search up the org tree for the most appropriate fund
1280         for my $org (@$org_path) {
1281             $fund = $mgr->editor->search_acq_fund(
1282                 {org => $org, code => $code, year => DateTime->now->year}, {idlist => 1})->[0];
1283             last if $fund;
1284         }
1285     }
1286     return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1287     $compiled{fund} = $fund;
1288     $mgr->cache($base_org, "fund.$code", $fund);
1289
1290
1291     # ---------------------------------------------------------------------
1292     # Owning lib
1293     my $sn = $compiled{owning_lib};
1294     return $killme->('no owning_lib defined') unless $sn;
1295     my $org_id = 
1296         $mgr->cache($base_org, "orgsn.$sn") ||
1297             $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1298     return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1299     $compiled{owning_lib} = $org_id;
1300     $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1301
1302
1303     # ---------------------------------------------------------------------
1304     # Circ Modifier
1305     my $mod;
1306     $code = $compiled{circ_modifier};
1307
1308     if($code) {
1309
1310         $mod = $mgr->cache($base_org, "mod.$code") ||
1311             $mgr->editor->retrieve_config_circ_modifier($code);
1312         return $killme->("invlalid circ_modifier $code") unless $mod;
1313         $mgr->cache($base_org, "mod.$code", $mod);
1314
1315     } else {
1316         # try the default
1317         $mod = get_default_circ_modifier($mgr, $base_org)
1318             or return $killme->('no circ_modifier defined');
1319     }
1320
1321     $compiled{circ_modifier} = $mod;
1322
1323
1324     # ---------------------------------------------------------------------
1325     # Shelving Location
1326     my $name = $compiled{copy_location};
1327     if($name) {
1328         my $loc = $mgr->cache($base_org, "copy_loc.$name");
1329         unless($loc) {
1330             for my $org (@$org_path) {
1331                 $loc = $mgr->editor->search_asset_copy_location(
1332                     {owning_lib => $org, name => $name}, {idlist => 1})->[0];
1333                 last if $loc;
1334             }
1335         }
1336         return $killme->("Invalid copy location $name") unless $loc;
1337         $compiled{copy_location} = $loc;
1338         $mgr->cache($base_org, "copy_loc.$name", $loc);
1339     }
1340
1341     return \%compiled;
1342 }
1343
1344
1345
1346 # ----------------------------------------------------------------------------
1347 # Workflow: Given an existing purchase order, import/create the bibs, 
1348 # callnumber and copy objects
1349 # ----------------------------------------------------------------------------
1350
1351 __PACKAGE__->register_method(
1352         method => 'create_po_assets',
1353         api_name        => 'open-ils.acq.purchase_order.assets.create',
1354         signature => {
1355         desc => q/Creates assets for each lineitem in the purchase order/,
1356         params => [
1357             {desc => 'Authentication token', type => 'string'},
1358             {desc => 'The purchase order id', type => 'number'},
1359         ],
1360         return => {desc => 'Streams a total versus completed counts object, event on error'}
1361     }
1362 );
1363
1364 sub create_po_assets {
1365     my($self, $conn, $auth, $po_id) = @_;
1366
1367     my $e = new_editor(authtoken=>$auth, xact=>1);
1368     return $e->die_event unless $e->checkauth;
1369     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1370
1371     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1372
1373     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1374
1375     # it's ugly, but it's fast.  Get the total count of lineitem detail objects to process
1376     my $lid_total = $e->json_query({
1377         select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] }, 
1378         from => {
1379             acqlid => {
1380                 jub => {
1381                     fkey => 'lineitem', 
1382                     field => 'id', 
1383                     join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1384                 }
1385             }
1386         }, 
1387         where => {'+acqpo' => {id => $po_id}}
1388     })->[0]->{id};
1389
1390     $mgr->total(scalar(@$li_ids) + $lid_total);
1391
1392     create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1393
1394     $e->xact_begin;
1395     update_purchase_order($mgr, $po) or return $e->die_event;
1396     $e->commit;
1397
1398     return $mgr->respond_complete;
1399 }
1400
1401
1402
1403 __PACKAGE__->register_method(
1404         method => 'create_purchase_order_api',
1405         api_name        => 'open-ils.acq.purchase_order.create',
1406         signature => {
1407         desc => 'Creates a new purchase order',
1408         params => [
1409             {desc => 'Authentication token', type => 'string'},
1410             {desc => 'purchase_order to create', type => 'object'}
1411         ],
1412         return => {desc => 'The purchase order id, Event on failure'}
1413     }
1414 );
1415
1416 sub create_purchase_order_api {
1417     my($self, $conn, $auth, $po, $args) = @_;
1418     $args ||= {};
1419
1420     my $e = new_editor(xact=>1, authtoken=>$auth);
1421     return $e->die_event unless $e->checkauth;
1422     return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1423     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1424
1425     # create the PO
1426     my %pargs = (ordering_agency => $e->requestor->ws_ou); # default
1427     $pargs{provider} = $po->provider if $po->provider;
1428     $pargs{ordering_agency} = $po->ordering_agency if $po->ordering_agency;
1429     $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1430
1431     my $li_ids = $$args{lineitems};
1432
1433     if($li_ids) {
1434
1435         for my $li_id (@$li_ids) { 
1436
1437             my $li = $e->retrieve_acq_lineitem([
1438                 $li_id,
1439                 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1440             ]) or return $e->die_event;
1441
1442             $li->provider($po->provider);
1443             $li->purchase_order($po->id);
1444             $li->state('pending-order');
1445             update_lineitem($mgr, $li) or return $e->die_event;
1446             $mgr->respond;
1447
1448             create_lineitem_debits($mgr, $li) or return $e->die_event;
1449         }
1450     }
1451
1452     # commit before starting the asset creation
1453     $e->xact_commit;
1454
1455     if($li_ids and $$args{create_assets}) {
1456         create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1457     }
1458
1459     return $mgr->respond_complete;
1460 }
1461
1462
1463 __PACKAGE__->register_method(
1464         method => 'lineitem_detail_CUD_batch',
1465         api_name => 'open-ils.acq.lineitem_detail.cud.batch',
1466     stream => 1,
1467         signature => {
1468         desc => q/Creates a new purchase order line item detail.  
1469             Additionally creates the associated fund_debit/,
1470         params => [
1471             {desc => 'Authentication token', type => 'string'},
1472             {desc => 'List of lineitem_details to create', type => 'array'},
1473         ],
1474         return => {desc => 'Streaming response of current position in the array'}
1475     }
1476 );
1477
1478 sub lineitem_detail_CUD_batch {
1479     my($self, $conn, $auth, $li_details) = @_;
1480
1481     my $e = new_editor(xact=>1, authtoken=>$auth);
1482     return $e->die_event unless $e->checkauth;
1483     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1484
1485     # XXX perms
1486
1487     $mgr->total(scalar(@$li_details));
1488     
1489     my %li_cache;
1490
1491     for my $lid (@$li_details) {
1492
1493         my $li = $li_cache{$lid->lineitem} || $e->retrieve_acq_lineitem($lid->lineitem);
1494
1495         if($lid->isnew) {
1496             create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
1497
1498         } elsif($lid->ischanged) {
1499             $e->update_acq_lineitem_detail($lid) or return $e->die_event;
1500
1501         } elsif($lid->isdeleted) {
1502             delete_lineitem_detail($mgr, $lid) or return $e->die_event;
1503         }
1504
1505         $mgr->respond(li => $li);
1506         $li_cache{$lid->lineitem} = $li;
1507     }
1508
1509     $e->commit;
1510     return $mgr->respond_complete;
1511 }
1512
1513
1514 __PACKAGE__->register_method(
1515         method => 'receive_po_api',
1516         api_name        => 'open-ils.acq.purchase_order.receive'
1517 );
1518
1519 sub receive_po_api {
1520     my($self, $conn, $auth, $po_id) = @_;
1521     my $e = new_editor(xact => 1, authtoken => $auth);
1522     return $e->die_event unless $e->checkauth;
1523     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1524
1525     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1526     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1527
1528     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1529
1530     for my $li_id (@$li_ids) {
1531         receive_lineitem($mgr, $li_id) or return $e->die_event;
1532         $mgr->respond;
1533     }
1534
1535     $po->state('received');
1536     update_purchase_order($mgr, $po) or return $e->die_event;
1537
1538     $e->commit;
1539     return $mgr->respond_complete;
1540 }
1541
1542
1543 __PACKAGE__->register_method(
1544         method => 'receive_lineitem_detail_api',
1545         api_name        => 'open-ils.acq.lineitem_detail.receive',
1546         signature => {
1547         desc => 'Mark a lineitem_detail as received',
1548         params => [
1549             {desc => 'Authentication token', type => 'string'},
1550             {desc => 'lineitem detail ID', type => 'number'}
1551         ],
1552         return => {desc => '1 on success, Event on error'}
1553     }
1554 );
1555
1556 sub receive_lineitem_detail_api {
1557     my($self, $conn, $auth, $lid_id) = @_;
1558
1559     my $e = new_editor(xact=>1, authtoken=>$auth);
1560     return $e->die_event unless $e->checkauth;
1561     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1562
1563     my $lid = $e->retrieve_acq_lineitem_detail([
1564         $lid_id, {
1565             flesh => 2,
1566             flesh_fields => {
1567                 acqlid => ['lineitem'],
1568                 jub => ['purchase_order']
1569             }
1570         }
1571     ]);
1572
1573     return $e->die_event unless $e->allowed(
1574         'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
1575
1576     receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
1577     $e->commit;
1578     return 1;
1579 }
1580
1581 __PACKAGE__->register_method(
1582         method => 'receive_lineitem_api',
1583         api_name        => 'open-ils.acq.lineitem.receive',
1584         signature => {
1585         desc => 'Mark a lineitem as received',
1586         params => [
1587             {desc => 'Authentication token', type => 'string'},
1588             {desc => 'lineitem detail ID', type => 'number'}
1589         ],
1590         return => {desc => '1 on success, Event on error'}
1591     }
1592 );
1593
1594 sub receive_lineitem_api {
1595     my($self, $conn, $auth, $li_id) = @_;
1596
1597     my $e = new_editor(xact=>1, authtoken=>$auth);
1598     return $e->die_event unless $e->checkauth;
1599     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1600
1601     my $li = $e->retrieve_acq_lineitem([
1602         $li_id, {
1603             flesh => 1,
1604             flesh_fields => {
1605                 jub => ['purchase_order']
1606             }
1607         }
1608     ]) or return $e->die_event;
1609
1610     return $e->die_event unless $e->allowed(
1611         'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
1612
1613     receive_lineitem($mgr, $li_id) or return $e->die_event;
1614
1615     $e->commit;
1616     $conn->respond_complete(1);
1617     $mgr->run_post_response_hooks;
1618     return undef;
1619 }
1620
1621
1622 __PACKAGE__->register_method(
1623         method => 'rollback_receive_po_api',
1624         api_name        => 'open-ils.acq.purchase_order.receive.rollback'
1625 );
1626
1627 sub rollback_receive_po_api {
1628     my($self, $conn, $auth, $po_id) = @_;
1629     my $e = new_editor(xact => 1, authtoken => $auth);
1630     return $e->die_event unless $e->checkauth;
1631     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1632
1633     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1634     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1635
1636     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1637
1638     for my $li_id (@$li_ids) {
1639         rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
1640         $mgr->respond;
1641     }
1642
1643     $po->state('on-order');
1644     update_purchase_order($mgr, $po) or return $e->die_event;
1645
1646     $e->commit;
1647     return $mgr->respond_complete;
1648 }
1649
1650
1651 __PACKAGE__->register_method(
1652         method => 'rollback_receive_lineitem_detail_api',
1653         api_name        => 'open-ils.acq.lineitem_detail.receive.rollback',
1654         signature => {
1655         desc => 'Mark a lineitem_detail as received',
1656         params => [
1657             {desc => 'Authentication token', type => 'string'},
1658             {desc => 'lineitem detail ID', type => 'number'}
1659         ],
1660         return => {desc => '1 on success, Event on error'}
1661     }
1662 );
1663
1664 sub rollback_receive_lineitem_detail_api {
1665     my($self, $conn, $auth, $lid_id) = @_;
1666
1667     my $e = new_editor(xact=>1, authtoken=>$auth);
1668     return $e->die_event unless $e->checkauth;
1669     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1670
1671     my $lid = $e->retrieve_acq_lineitem_detail([
1672         $lid_id, {
1673             flesh => 2,
1674             flesh_fields => {
1675                 acqlid => ['lineitem'],
1676                 jub => ['purchase_order']
1677             }
1678         }
1679     ]);
1680     my $li = $lid->lineitem;
1681     my $po = $li->purchase_order;
1682
1683     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1684     rollback_receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
1685
1686     $li->state('on-order');
1687     $po->state('on-order');
1688     udpate_lineitem($mgr, $li) or return $e->die_event;
1689     udpate_purchase_order($mgr, $po) or return $e->die_event;
1690
1691     $e->commit;
1692     return 1;
1693 }
1694
1695 __PACKAGE__->register_method(
1696         method => 'rollback_receive_lineitem_api',
1697         api_name        => 'open-ils.acq.lineitem.receive.rollback',
1698         signature => {
1699         desc => 'Mark a lineitem as received',
1700         params => [
1701             {desc => 'Authentication token', type => 'string'},
1702             {desc => 'lineitem detail ID', type => 'number'}
1703         ],
1704         return => {desc => '1 on success, Event on error'}
1705     }
1706 );
1707
1708 sub rollback_receive_lineitem_api {
1709     my($self, $conn, $auth, $li_id) = @_;
1710
1711     my $e = new_editor(xact=>1, authtoken=>$auth);
1712     return $e->die_event unless $e->checkauth;
1713     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1714
1715     my $li = $e->retrieve_acq_lineitem_detail([
1716         $li_id, {
1717             flesh => 1,
1718             flesh_fields => {
1719                 jub => ['purchase_order']
1720             }
1721         }
1722     ]);
1723     my $po = $li->purchase_order;
1724
1725     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1726
1727     rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
1728
1729     $po->state('on-order');
1730     update_purchase_order($mgr, $po) or return $e->die_event;
1731
1732     $e->commit;
1733     return 1;
1734 }
1735
1736
1737 __PACKAGE__->register_method(
1738         method => 'set_lineitem_price_api',
1739         api_name        => 'open-ils.acq.lineitem.price.set',
1740         signature => {
1741         desc => 'Set lineitem price.  If debits already exist, update them as well',
1742         params => [
1743             {desc => 'Authentication token', type => 'string'},
1744             {desc => 'lineitem ID', type => 'number'}
1745         ],
1746         return => {desc => 'status blob, Event on error'}
1747     }
1748 );
1749
1750 sub set_lineitem_price_api {
1751     my($self, $conn, $auth, $li_id, $price, $currency) = @_;
1752
1753     my $e = new_editor(xact=>1, authtoken=>$auth);
1754     return $e->die_event unless $e->checkauth;
1755     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1756
1757     # XXX perms
1758
1759     my $li = $e->retrieve_acq_lineitem($li_id) or return $e->die_event;
1760
1761     # update the local attr for estimated price
1762     set_lineitem_attr(
1763         $mgr, 
1764         attr_name => 'estimated_price',
1765         attr_type => 'lineitem_local_attr_definition',
1766         attr_value => $price,
1767         lineitem => $li_id
1768     ) or return $e->die_event;
1769
1770     my $lid_ids = $e->search_acq_lineitem_detail(
1771         {lineitem => $li_id, fund_debit => {'!=' => undef}}, 
1772         {idlist => 1}
1773     );
1774
1775     for my $lid_id (@$lid_ids) {
1776
1777         my $lid = $e->retrieve_acq_lineitem_detail([
1778             $lid_id, {
1779             flesh => 1, flesh_fields => {acqlid => ['fund', 'fund_debit']}}
1780         ]);
1781
1782         # onless otherwise specified, assume currency of new price is same as currency type of the fund
1783         $currency ||= $lid->fund->currency_type;
1784         my $amount = $price;
1785
1786         if($lid->fund->currency_type ne $currency) {
1787             $amount = currency_conversion($mgr, $currency, $lid->fund->currency_type, $price);
1788         }
1789         
1790         $lid->fund_debit->origin_currency_type($currency);
1791         $lid->fund_debit->origin_amount($price);
1792         $lid->fund_debit->amount($amount);
1793
1794         $e->update_acq_fund_debit($lid->fund_debit) or return $e->die_event;
1795         $mgr->add_lid;
1796         $mgr->respond;
1797     }
1798
1799     $e->commit;
1800     return $mgr->respond_complete;
1801 }
1802
1803
1804 __PACKAGE__->register_method(
1805         method => 'clone_picklist_api',
1806         api_name        => 'open-ils.acq.picklist.clone',
1807         signature => {
1808         desc => 'Clones a picklist, including lineitem and lineitem details',
1809         params => [
1810             {desc => 'Authentication token', type => 'string'},
1811             {desc => 'Picklist ID', type => 'number'},
1812             {desc => 'New Picklist Name', type => 'string'}
1813         ],
1814         return => {desc => 'status blob, Event on error'}
1815     }
1816 );
1817
1818 sub clone_picklist_api {
1819     my($self, $conn, $auth, $pl_id, $name) = @_;
1820
1821     my $e = new_editor(xact=>1, authtoken=>$auth);
1822     return $e->die_event unless $e->checkauth;
1823     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1824
1825     my $old_pl = $e->retrieve_acq_picklist($pl_id);
1826     my $new_pl = create_picklist($mgr, %{$old_pl->to_bare_hash}, name => $name) or return $e->die_event;
1827
1828     my $li_ids = $e->search_acq_lineitem({picklist => $pl_id}, {idlist => 1});
1829
1830     for my $li_id (@$li_ids) {
1831
1832         # copy the lineitems
1833         my $li = $e->retrieve_acq_lineitem($li_id);
1834         my $new_li = create_lineitem($mgr, %{$li->to_bare_hash}, picklist => $new_pl->id) or return $e->die_event;
1835
1836         my $lid_ids = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist => 1});
1837         for my $lid_id (@$lid_ids) {
1838
1839             # copy the lineitem details
1840             my $lid = $e->retrieve_acq_lineitem_detail($lid_id);
1841             create_lineitem_detail($mgr, %{$lid->to_bare_hash}, lineitem => $new_li->id) or return $e->die_event;
1842         }
1843
1844         $mgr->respond;
1845     }
1846
1847     $e->commit;
1848     return $mgr->respond_complete;
1849 }
1850
1851
1852 __PACKAGE__->register_method(
1853         method => 'merge_picklist_api',
1854         api_name        => 'open-ils.acq.picklist.merge',
1855         signature => {
1856         desc => 'Merges 2 or more picklists into a single list',
1857         params => [
1858             {desc => 'Authentication token', type => 'string'},
1859             {desc => 'Lead Picklist ID', type => 'number'},
1860             {desc => 'List of subordinate picklist IDs', type => 'array'}
1861         ],
1862         return => {desc => 'status blob, Event on error'}
1863     }
1864 );
1865
1866 sub merge_picklist_api {
1867     my($self, $conn, $auth, $lead_pl, $pl_list) = @_;
1868
1869     my $e = new_editor(xact=>1, authtoken=>$auth);
1870     return $e->die_event unless $e->checkauth;
1871     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1872
1873     # XXX perms on each picklist modified
1874
1875     # point all of the lineitems at the lead picklist
1876     my $li_ids = $e->search_acq_lineitem({picklist => $pl_list}, {idlist => 1});
1877
1878     for my $li_id (@$li_ids) {
1879         my $li = $e->retrieve_acq_lineitem($li_id);
1880         $li->picklist($lead_pl);
1881         update_lineitem($mgr, $li) or return $e->die_event;
1882         $mgr->respond;
1883     }
1884
1885     # now delete the subordinate lists
1886     for my $pl_id (@$pl_list) {
1887         my $pl = $e->retrieve_acq_picklist($pl_id);
1888         $e->delete_acq_picklist($pl) or return $e->die_event;
1889     }
1890
1891     $e->commit;
1892     return $mgr->respond_complete;
1893 }
1894
1895
1896 __PACKAGE__->register_method(
1897         method => 'delete_picklist_api',
1898         api_name        => 'open-ils.acq.picklist.delete',
1899         signature => {
1900         desc => q/Deletes a picklist.  It also deletes any lineitems in the "new" state.  
1901             Other attached lineitems are detached'/,
1902         params => [
1903             {desc => 'Authentication token', type => 'string'},
1904             {desc => 'Picklist ID to delete', type => 'number'}
1905         ],
1906         return => {desc => '1 on success, Event on error'}
1907     }
1908 );
1909
1910 sub delete_picklist_api {
1911     my($self, $conn, $auth, $picklist_id) = @_;
1912     my $e = new_editor(xact=>1, authtoken=>$auth);
1913     return $e->die_event unless $e->checkauth;
1914     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1915     my $pl = $e->retrieve_acq_picklist($picklist_id) or return $e->die_event;
1916     delete_picklist($mgr, $pl) or return $e->die_event;
1917     $e->commit;
1918     return $mgr->respond_complete;
1919 }
1920
1921
1922
1923 __PACKAGE__->register_method(
1924         method => 'activate_purchase_order',
1925         api_name        => 'open-ils.acq.purchase_order.activate',
1926         signature => {
1927         desc => q/Activates a purchase order.  This updates the status of the PO
1928             and Lineitems to 'on-order'.  Activated PO's are ready for EDI delivery
1929             if appropriate./,
1930         params => [
1931             {desc => 'Authentication token', type => 'string'},
1932             {desc => 'Purchase ID', type => 'number'}
1933         ],
1934         return => {desc => '1 on success, Event on error'}
1935     }
1936 );
1937
1938 sub activate_purchase_order {
1939     my($self, $conn, $auth, $po_id) = @_;
1940     my $e = new_editor(xact=>1, authtoken=>$auth);
1941     return $e->die_event unless $e->checkauth;
1942     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1943
1944     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1945     return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1946
1947     $po->state('on-order');
1948     update_purchase_order($mgr, $po) or return $e->die_event;
1949
1950     my $query = [
1951         {purchase_order => $po_id, state => 'pending-order'},
1952         {limit => 1}
1953     ];
1954
1955     while( my $li = $e->search_acq_lineitem($query)->[0] ) {
1956         $li->state('on-order');
1957         update_lineitem($mgr, $li) or return $e->die_event;
1958         $mgr->post_process( sub { create_lineitem_status_events($mgr, $li->id, 'aur.ordered'); });
1959         $mgr->respond;
1960     }
1961
1962     $e->commit;
1963     $conn->respond_complete(1);
1964     $mgr->run_post_response_hooks;
1965     return undef;
1966 }
1967
1968
1969 __PACKAGE__->register_method(
1970         method => 'split_purchase_order_by_lineitems',
1971         api_name        => 'open-ils.acq.purchase_order.split_by_lineitems',
1972         signature => {
1973         desc => q/Splits a PO into many POs, 1 per lineitem.  Only works for
1974         POs a) with more than one lineitems, and b) in the "pending" state./,
1975         params => [
1976             {desc => 'Authentication token', type => 'string'},
1977             {desc => 'Purchase order ID', type => 'number'}
1978         ],
1979         return => {desc => 'list of new PO IDs on success, Event on error'}
1980     }
1981 );
1982
1983 sub split_purchase_order_by_lineitems {
1984     my ($self, $conn, $auth, $po_id) = @_;
1985
1986     my $e = new_editor("xact" => 1, "authtoken" => $auth);
1987     return $e->die_event unless $e->checkauth;
1988
1989     my $po = $e->retrieve_acq_purchase_order([
1990         $po_id, {
1991             "flesh" => 1,
1992             "flesh_fields" => {"acqpo" => [qw/lineitems notes/]}
1993         }
1994     ]) or return $e->die_event;
1995
1996     return $e->die_event
1997         unless $e->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
1998
1999     unless ($po->state eq "pending") {
2000         $e->rollback;
2001         return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_LATE");
2002     }
2003
2004     unless (@{$po->lineitems} > 1) {
2005         $e->rollback;
2006         return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_SHORT");
2007     }
2008
2009     # To split an existing PO into many, it seems unwise to just delete the
2010     # original PO, so we'll instead detach all of the original POs' lineitems
2011     # but the first, then create new POs for each of the remaining LIs, and
2012     # then attach the LIs to their new POs.
2013
2014     my @po_ids = ($po->id);
2015     my @moving_li = @{$po->lineitems};
2016     shift @moving_li;    # discard first LI
2017
2018     foreach my $li (@moving_li) {
2019         my $new_po = $po->clone;
2020         $new_po->clear_id;
2021         $new_po->clear_name;
2022         $new_po->creator($e->requestor->id);
2023         $new_po->editor($e->requestor->id);
2024         $new_po->owner($e->requestor->id);
2025         $new_po->edit_time("now");
2026         $new_po->create_time("now");
2027
2028         $new_po = $e->create_acq_purchase_order($new_po);
2029
2030         # Clone any notes attached to the old PO and attach to the new one.
2031         foreach my $note (@{$po->notes}) {
2032             my $new_note = $note->clone;
2033             $new_note->clear_id;
2034             $new_note->edit_time("now");
2035             $new_note->purchase_order($new_po->id);
2036             $e->create_acq_po_note($new_note);
2037         }
2038
2039         $li->edit_time("now");
2040         $li->purchase_order($new_po->id);
2041         $e->update_acq_lineitem($li);
2042
2043         push @po_ids, $new_po->id;
2044     }
2045
2046     $po->edit_time("now");
2047     $e->update_acq_purchase_order($po);
2048
2049     return \@po_ids if $e->commit;
2050     return $e->die_event;
2051 }
2052
2053
2054 __PACKAGE__->register_method(
2055         method => 'cancel_lineitem_api',
2056         api_name        => 'open-ils.acq.lineitem.cancel',
2057         signature => {
2058         desc => q/Cancels an on-order lineitem/,
2059         params => [
2060             {desc => 'Authentication token', type => 'string'},
2061             {desc => 'Lineitem ID to cancel', type => 'number'},
2062             {desc => 'Cancel Cause ID', type => 'number'}
2063         ],
2064         return => {desc => '1 on success, Event on error'}
2065     }
2066 );
2067
2068 sub cancel_lineitem_api {
2069     my($self, $conn, $auth, $li_id, $cancel_cause) = @_;
2070
2071     my $e = new_editor(xact=>1, authtoken=>$auth);
2072     return $e->die_event unless $e->checkauth;
2073     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2074
2075     my $li = $e->retrieve_acq_lineitem([$li_id, 
2076         {flesh => 1, flesh_fields => {jub => [q/purchase_order/]}}]);
2077
2078     unless( $li->purchase_order and ($li->state eq 'on-order' or $li->state eq 'pending-order') ) {
2079         $e->rollback;
2080         return OpenILS::Event->new('BAD_PARAMS') 
2081     }
2082
2083     return $e->die_event unless 
2084         $e->allowed('CREATE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
2085
2086     $li->state('cancelled');
2087
2088     # TODO delete the associated fund debits?
2089     # TODO add support for cancel reasons
2090     # TODO who/what/where/how do we indicate this change for electronic orders?
2091
2092     update_lineitem($mgr, $li) or return $e->die_event;
2093     $e->commit;
2094
2095     $conn->respond_complete($li);
2096     create_lineitem_status_events($mgr, $li_id, 'aur.cancelled');
2097     return undef;
2098 }
2099
2100
2101 1;