1 package OpenILS::Application::Acq::BatchManager;
2 use OpenSRF::AppSession;
3 use OpenSRF::EX qw/:try/;
4 use strict; use warnings;
7 my($class, %args) = @_;
8 my $self = bless(\%args, $class);
16 purchase_order => undef,
22 $self->{ingest_queue} = [];
24 $self->throttle(5) unless $self->throttle;
25 $self->{post_proc_queue} = [];
26 $self->{last_respond_progress} = 0;
32 $self->{conn} = $val if $val;
37 $self->{throttle} = $val if $val;
38 return $self->{throttle};
41 my($self, %other_args) = @_;
42 if($self->throttle and not %other_args) {
44 ($self->{args}->{progress} - $self->{last_respond_progress}) >= $self->throttle
47 $self->conn->respond({ %{$self->{args}}, %other_args });
48 $self->{last_respond_progress} = $self->{args}->{progress};
50 sub respond_complete {
51 my($self, %other_args) = @_;
53 $self->conn->respond_complete({ %{$self->{args}}, %other_args });
54 $self->run_post_response_hooks;
58 # run the post response hook subs, shifting them off as we go
59 sub run_post_response_hooks {
61 (shift @{$self->{post_proc_queue}})->() while @{$self->{post_proc_queue}};
64 # any subs passed to this method will be run after the call to respond_complete
67 push(@{$self->{post_proc_queue}}, $sub);
72 $self->{args}->{total} = $val if defined $val;
73 $self->{args}->{maximum} = $self->{args}->{total};
74 return $self->{args}->{total};
78 $self->{args}->{purchase_order} = $val if $val;
83 $self->{args}->{picklist} = $val if $val;
88 $self->{args}->{lid} += 1;
89 $self->{args}->{progress} += 1;
94 $self->{args}->{li} += 1;
95 $self->{args}->{progress} += 1;
100 $self->{args}->{copies} += 1;
101 $self->{args}->{progress} += 1;
106 $self->{args}->{bibs} += 1;
107 $self->{args}->{progress} += 1;
111 my($self, $amount) = @_;
112 $self->{args}->{debits_accrued} += $amount;
113 $self->{args}->{progress} += 1;
117 my($self, $editor) = @_;
118 $self->{editor} = $editor if defined $editor;
119 return $self->{editor};
123 $self->{args}->{complete} = 1;
128 my($self, $val) = @_;
129 $self->{ingest_ses} = $val if $val;
130 return $self->{ingest_ses};
133 sub push_ingest_queue {
134 my($self, $rec_id) = @_;
136 $self->ingest_ses(OpenSRF::AppSession->connect('open-ils.ingest'))
137 unless $self->ingest_ses;
139 my $req = $self->ingest_ses->request('open-ils.ingest.full.biblio.record', $rec_id);
141 push(@{$self->{ingest_queue}}, $req);
144 sub process_ingest_records {
146 return unless @{$self->{ingest_queue}};
148 for my $req (@{$self->{ingest_queue}}) {
152 $self->{args}->{indexed} += 1;
153 $self->{args}->{progress} += 1;
158 $self->ingest_ses->disconnect;
163 my($self, $org, $key, $val) = @_;
164 $self->{cache}->{$org} = {} unless $self->{cache}->{org};
165 $self->{cache}->{$org}->{$key} = $val if defined $val;
166 return $self->{cache}->{$org}->{$key};
170 package OpenILS::Application::Acq::Order;
171 use base qw/OpenILS::Application/;
172 use strict; use warnings;
173 # ----------------------------------------------------------------------------
174 # Break up each component of the order process and pieces into managable
175 # actions that can be shared across different workflows
176 # ----------------------------------------------------------------------------
178 use OpenSRF::Utils::Logger qw(:logger);
179 use OpenSRF::Utils::JSON;
180 use OpenSRF::AppSession;
181 use OpenILS::Utils::Fieldmapper;
182 use OpenILS::Utils::CStoreEditor q/:funcs/;
183 use OpenILS::Const qw/:const/;
184 use OpenSRF::EX q/:try/;
185 use OpenILS::Application::AppUtils;
186 use OpenILS::Application::Cat::BibCommon;
187 use OpenILS::Application::Cat::AssetCommon;
191 my $U = 'OpenILS::Application::AppUtils';
194 # ----------------------------------------------------------------------------
196 # ----------------------------------------------------------------------------
197 sub create_lineitem {
198 my($mgr, %args) = @_;
199 my $li = Fieldmapper::acq::lineitem->new;
200 $li->creator($mgr->editor->requestor->id);
201 $li->selector($li->creator);
202 $li->editor($li->creator);
203 $li->create_time('now');
204 $li->edit_time('now');
206 $li->$_($args{$_}) for keys %args;
209 return $mgr->editor->create_acq_lineitem($li);
212 sub update_lineitem {
214 $li->edit_time('now');
215 $li->editor($mgr->editor->requestor->id);
217 return $li if $mgr->editor->update_acq_lineitem($li);
222 # ----------------------------------------------------------------------------
223 # Create real holds from patron requests for a given lineitem
224 # ----------------------------------------------------------------------------
225 sub promote_lineitem_holds {
228 my $requests = $mgr->editor->search_acq_user_request(
229 { lineitem => $li->id,
231 [ { need_before => {'>' => 'now'} },
232 { need_before => undef }
237 for my $request ( @$requests ) {
239 $request->eg_bib( $li->eg_bib_id );
240 $mgr->editor->update_acq_user_request( $request ) or return 0;
242 next unless ($U->is_true( $request->hold ));
244 my $hold = Fieldmapper::action::hold_request->new;
245 $hold->usr( $request->usr );
246 $hold->requestor( $request->usr );
247 $hold->request_time( $request->request_date );
248 $hold->pickup_lib( $request->pickup_lib );
249 $hold->request_lib( $request->pickup_lib );
250 $hold->selection_ou( $request->pickup_lib );
251 $hold->phone_notify( $request->phone_notify );
252 $hold->email_notify( $request->email_notify );
253 $hold->expire_time( $request->need_before );
255 if ($request->holdable_formats) {
256 my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
258 $hold->hold_type( 'M' );
259 $hold->holdable_formats( $request->holdable_formats );
260 $hold->target( $mrm->metarecord );
264 if (!$hold->target) {
265 $hold->hold_type( 'T' );
266 $hold->target( $li->eg_bib_id );
269 $mgr->editor->create_actor_hold_request( $hold ) or return 0;
275 sub delete_lineitem {
277 $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
279 # delete the attached lineitem_details
280 my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
281 for my $lid_id (@$lid_ids) {
282 return 0 unless delete_lineitem_detail($mgr, $lid_id);
286 return $mgr->editor->delete_acq_lineitem($li);
289 # begins and commit transactions as it goes
290 sub create_lineitem_list_assets {
291 my($mgr, $li_ids) = @_;
292 return undef if check_import_li_marc_perms($mgr, $li_ids);
294 # create the bibs/volumes/copies and ingest the records
295 for my $li_id (@$li_ids) {
296 $mgr->editor->xact_begin;
297 my $data = create_lineitem_assets($mgr, $li_id) or return undef;
298 $mgr->editor->xact_commit;
299 # XXX ingest is in-db now
300 #$mgr->push_ingest_queue($data->{li}->eg_bib_id) if $data->{new_bib};
303 $mgr->process_ingest_records;
307 # returns event on error, undef on success
308 sub check_import_li_marc_perms {
309 my($mgr, $li_ids) = @_;
311 # if there are any order records that are not linked to
312 # in-db bib records, verify staff has perms to import order records
313 my $order_li = $mgr->editor->search_acq_lineitem(
314 [{id => $li_ids, eg_bib_id => undef}, {limit => 1}], {idlist => 1})->[0];
317 return $mgr->editor->die_event unless
318 $mgr->editor->allowed('IMPORT_ACQ_LINEITEM_BIB_RECORD');
325 # ----------------------------------------------------------------------------
326 # if all of the lineitem details for this lineitem have
327 # been received, mark the lineitem as received
328 # returns 1 on non-received, li on received, 0 on error
329 # ----------------------------------------------------------------------------
330 sub check_lineitem_received {
331 my($mgr, $li_id) = @_;
333 my $non_recv = $mgr->editor->search_acq_lineitem_detail(
334 {recv_time => undef, lineitem => $li_id}, {idlist=>1});
336 return 1 if @$non_recv;
338 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
339 $li->state('received');
340 return update_lineitem($mgr, $li);
343 sub receive_lineitem {
344 my($mgr, $li_id, $skip_complete_check) = @_;
345 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
347 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
348 {lineitem => $li_id, recv_time => undef}, {idlist => 1});
350 for my $lid_id (@$lid_ids) {
351 receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
355 $li->state('received');
356 update_lineitem($mgr, $li) or return 0;
358 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
360 return 1 if $skip_complete_check;
362 return check_purchase_order_received($mgr, $li->purchase_order);
365 sub rollback_receive_lineitem {
366 my($mgr, $li_id) = @_;
367 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
369 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
370 {lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
372 for my $lid_id (@$lid_ids) {
373 rollback_receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
377 $li->state('on-order');
378 return update_lineitem($mgr, $li);
382 sub create_lineitem_status_events {
383 my($mgr, $li_id, $hook) = @_;
385 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
387 my $user_reqs = $mgr->editor->search_acq_user_request([
388 {lineitem => $li_id},
389 {flesh => 1, flesh_fields => {aur => ['usr']}}
392 for my $user_req (@$user_reqs) {
393 my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $user_req->usr->home_ou);
401 # ----------------------------------------------------------------------------
403 # ----------------------------------------------------------------------------
404 sub create_lineitem_detail {
405 my($mgr, %args) = @_;
406 my $lid = Fieldmapper::acq::lineitem_detail->new;
407 $lid->$_($args{$_}) for keys %args;
410 return $mgr->editor->create_acq_lineitem_detail($lid);
414 # flesh out any required data with default values where appropriate
415 sub complete_lineitem_detail {
417 unless($lid->barcode) {
418 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
419 $lid->barcode($pfx.$lid->id);
422 unless($lid->cn_label) {
423 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
424 $lid->cn_label($pfx.$lid->id);
427 if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
428 $lid->location($loc);
431 if(!$lid->circ_modifier and my $mod = get_default_circ_modifier($mgr, $lid->owning_lib)) {
432 $lid->circ_modifier($mod);
435 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
439 sub get_default_circ_modifier {
441 my $mod = $mgr->cache($org, 'def_circ_mod');
443 $mod = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier');
444 return $mgr->cache($org, 'def_circ_mod', $mod) if $mod;
448 sub delete_lineitem_detail {
450 $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
451 return $mgr->editor->delete_acq_lineitem_detail($lid);
455 sub receive_lineitem_detail {
456 my($mgr, $lid_id, $skip_complete_check) = @_;
457 my $e = $mgr->editor;
459 my $lid = $e->retrieve_acq_lineitem_detail([
463 acqlid => ['fund_debit']
468 return 1 if $lid->recv_time;
470 $lid->recv_time('now');
471 $e->update_acq_lineitem_detail($lid) or return 0;
473 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
474 $copy->status(OILS_COPY_STATUS_IN_PROCESS);
475 $copy->edit_date('now');
476 $copy->editor($e->requestor->id);
477 $e->update_asset_copy($copy) or return 0;
479 if($lid->fund_debit) {
480 $lid->fund_debit->encumbrance('f');
481 $e->update_acq_fund_debit($lid->fund_debit) or return 0;
486 return 1 if $skip_complete_check;
488 my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
489 return 1 if $li == 1; # li not received
491 return check_purchase_order_received($mgr, $li->purchase_order);
495 sub rollback_receive_lineitem_detail {
496 my($mgr, $lid_id) = @_;
497 my $e = $mgr->editor;
499 my $lid = $e->retrieve_acq_lineitem_detail([
503 acqlid => ['fund_debit']
508 return 1 unless $lid->recv_time;
510 $lid->clear_recv_time;
511 $e->update_acq_lineitem_detail($lid) or return 0;
513 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
514 $copy->status(OILS_COPY_STATUS_ON_ORDER);
515 $copy->edit_date('now');
516 $copy->editor($e->requestor->id);
517 $e->update_asset_copy($copy) or return 0;
519 if($lid->fund_debit) {
520 $lid->fund_debit->encumbrance('t');
521 $e->update_acq_fund_debit($lid->fund_debit) or return 0;
528 # ----------------------------------------------------------------------------
530 # ----------------------------------------------------------------------------
531 sub set_lineitem_attr {
532 my($mgr, %args) = @_;
533 my $attr_type = $args{attr_type};
535 # first, see if it's already set. May just need to overwrite it
536 my $attr = $mgr->editor->search_acq_lineitem_attr({
537 lineitem => $args{lineitem},
538 attr_type => $args{attr_type},
539 attr_name => $args{attr_name}
543 $attr->attr_value($args{attr_value});
544 return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
549 $attr = Fieldmapper::acq::lineitem_attr->new;
550 $attr->$_($args{$_}) for keys %args;
552 unless($attr->definition) {
553 my $find = "search_acq_$attr_type";
554 my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
555 $attr->definition($attr_def_id);
557 return $mgr->editor->create_acq_lineitem_attr($attr);
563 my $attrs = $li->attributes;
564 my ($marc_estimated, $local_estimated, $local_actual, $prov_estimated, $prov_actual);
566 for my $attr (@$attrs) {
567 if($attr->attr_name eq 'estimated_price') {
568 $local_estimated = $attr->attr_value
569 if $attr->attr_type eq 'lineitem_local_attr_definition';
570 $prov_estimated = $attr->attr_value
571 if $attr->attr_type eq 'lineitem_prov_attr_definition';
572 $marc_estimated = $attr->attr_value
573 if $attr->attr_type eq 'lineitem_marc_attr_definition';
575 } elsif($attr->attr_name eq 'actual_price') {
576 $local_actual = $attr->attr_value
577 if $attr->attr_type eq 'lineitem_local_attr_definition';
578 $prov_actual = $attr->attr_value
579 if $attr->attr_type eq 'lineitem_prov_attr_definition';
583 return ($local_actual, 1) if $local_actual;
584 return ($prov_actual, 2) if $prov_actual;
585 return ($local_estimated, 1) if $local_estimated;
586 return ($prov_estimated, 2) if $prov_estimated;
587 return ($marc_estimated, 3);
591 # ----------------------------------------------------------------------------
593 # ----------------------------------------------------------------------------
594 sub create_lineitem_debits {
595 my($mgr, $li, $price, $ptype) = @_;
597 ($price, $ptype) = get_li_price($li) unless $price;
600 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
601 $mgr->editor->rollback;
605 unless($li->provider) {
606 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
607 $mgr->editor->rollback;
611 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
612 {lineitem => $li->id},
616 for my $lid_id (@$lid_ids) {
618 my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
621 flesh_fields => {acqlid => ['fund']}
625 create_lineitem_detail_debit($mgr, $li, $lid, $price, $ptype) or return 0;
634 # ptype 1=local, 2=provider, 3=marc
635 sub create_lineitem_detail_debit {
636 my($mgr, $li, $lid, $price, $ptype) = @_;
638 my $li_id = ref($li) ? $li->id : $li;
640 unless(ref $li and ref $li->provider) {
641 $li = $mgr->editor->retrieve_acq_lineitem([
644 flesh_fields => {jub => ['provider']},
649 unless(ref $lid and ref $lid->fund) {
650 $lid = $mgr->editor->retrieve_acq_lineitem_detail([
653 flesh_fields => {acqlid => ['fund']}
658 my $ctype = $lid->fund->currency_type;
661 if($ptype == 2) { # price from vendor
662 $ctype = $li->provider->currency_type;
663 $amount = currency_conversion($mgr, $ctype, $lid->fund->currency_type, $price);
666 my $debit = create_fund_debit(
668 fund => $lid->fund->id,
669 origin_amount => $price,
670 origin_currency_type => $ctype,
674 $lid->fund_debit($debit->id);
675 $lid->fund($lid->fund->id);
676 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
681 # ----------------------------------------------------------------------------
683 # ----------------------------------------------------------------------------
684 sub create_fund_debit {
685 my($mgr, %args) = @_;
687 # Verify the fund is not being spent beyond the hard stop amount
688 my $fund = $mgr->editor->retrieve_acq_fund($args{fund}) or return 0;
690 if($fund->balance_stop_percent) {
692 my $balance = $mgr->editor->search_acq_fund_combined_balance({fund => $fund->id})->[0];
693 my $allocations = $mgr->editor->search_acq_fund_allocation_total({fund => $fund->id})->[0];
694 $balance = ($balance) ? $balance->amount : 0;
695 $allocations = ($allocations) ? $allocations->amount : 0;
698 $allocations == 0 || # if no allocations were ever made, assume we have hit the stop percent
699 ( ( ( ($balance - $args{amount}) / $allocations ) * 100 ) < $fund->balance_stop_percent))
701 $mgr->editor->event(OpenILS::Event->new(
702 'FUND_EXCEEDS_STOP_PERCENT',
703 payload => {fund => $fund->id, debit_amount => $args{amount}}
709 my $debit = Fieldmapper::acq::fund_debit->new;
710 $debit->debit_type('purchase');
711 $debit->encumbrance('t');
712 $debit->$_($args{$_}) for keys %args;
714 $mgr->add_debit($debit->amount);
715 return $mgr->editor->create_acq_fund_debit($debit);
718 sub currency_conversion {
719 my($mgr, $src_currency, $dest_currency, $amount) = @_;
720 my $result = $mgr->editor->json_query(
721 {from => ['acq.exchange_ratio', $src_currency, $dest_currency, $amount]});
722 return $result->[0]->{'acq.exchange_ratio'};
726 # ----------------------------------------------------------------------------
728 # ----------------------------------------------------------------------------
729 sub create_picklist {
730 my($mgr, %args) = @_;
731 my $picklist = Fieldmapper::acq::picklist->new;
732 $picklist->creator($mgr->editor->requestor->id);
733 $picklist->owner($picklist->creator);
734 $picklist->editor($picklist->creator);
735 $picklist->create_time('now');
736 $picklist->edit_time('now');
737 $picklist->org_unit($mgr->editor->requestor->ws_ou);
738 $picklist->owner($mgr->editor->requestor->id);
739 $picklist->$_($args{$_}) for keys %args;
741 $mgr->picklist($picklist);
742 return $mgr->editor->create_acq_picklist($picklist);
745 sub update_picklist {
746 my($mgr, $picklist) = @_;
747 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
748 $picklist->edit_time('now');
749 $picklist->editor($mgr->editor->requestor->id);
750 $mgr->picklist($picklist);
751 return $picklist if $mgr->editor->update_acq_picklist($picklist);
755 sub delete_picklist {
756 my($mgr, $picklist) = @_;
757 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
759 # delete all 'new' lineitems
760 my $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => 'new'}, {idlist => 1});
761 for my $li_id (@$li_ids) {
762 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
763 return 0 unless delete_lineitem($mgr, $li);
767 # detach all non-'new' lineitems
768 $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}}, {idlist => 1});
769 for my $li_id (@$li_ids) {
770 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
772 return 0 unless update_lineitem($mgr, $li);
776 # remove any picklist-specific object perms
777 my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
779 return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
782 return $mgr->editor->delete_acq_picklist($picklist);
785 # ----------------------------------------------------------------------------
787 # ----------------------------------------------------------------------------
788 sub update_purchase_order {
790 $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
791 $po->editor($mgr->editor->requestor->id);
792 $po->edit_time('now');
793 $mgr->purchase_order($po);
794 return $po if $mgr->editor->update_acq_purchase_order($po);
798 sub create_purchase_order {
799 my($mgr, %args) = @_;
801 # verify the chosen provider is still active
802 my $provider = $mgr->editor->retrieve_acq_provider($args{provider}) or return 0;
803 unless($U->is_true($provider->active)) {
804 $logger->error("provider is not active. cannot create PO");
805 $mgr->editor->event(OpenILS::Event->new('ACQ_PROVIDER_INACTIVE'));
809 my $po = Fieldmapper::acq::purchase_order->new;
810 $po->creator($mgr->editor->requestor->id);
811 $po->editor($mgr->editor->requestor->id);
812 $po->owner($mgr->editor->requestor->id);
813 $po->edit_time('now');
814 $po->create_time('now');
815 $po->state('pending');
816 $po->ordering_agency($mgr->editor->requestor->ws_ou);
817 $po->$_($args{$_}) for keys %args;
819 $mgr->purchase_order($po);
820 return $mgr->editor->create_acq_purchase_order($po);
823 # ----------------------------------------------------------------------------
824 # if all of the lineitems for this PO are received,
825 # mark the PO as received
826 # ----------------------------------------------------------------------------
827 sub check_purchase_order_received {
828 my($mgr, $po_id) = @_;
830 my $non_recv_li = $mgr->editor->search_acq_lineitem(
831 { purchase_order => $po_id,
832 state => {'!=' => 'received'}
835 return 1 if @$non_recv_li;
837 my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
838 $po->state('received');
839 return update_purchase_order($mgr, $po);
843 # ----------------------------------------------------------------------------
844 # Bib, Callnumber, and Copy data
845 # ----------------------------------------------------------------------------
847 sub create_lineitem_assets {
848 my($mgr, $li_id) = @_;
851 my $li = $mgr->editor->retrieve_acq_lineitem([
854 flesh_fields => {jub => ['purchase_order', 'attributes']}
858 # -----------------------------------------------------------------
859 # first, create the bib record if necessary
860 # -----------------------------------------------------------------
862 unless($li->eg_bib_id) {
863 create_bib($mgr, $li) or return 0;
868 # -----------------------------------------------------------------
869 # The lineitem is going live, promote user request holds to real holds
870 # -----------------------------------------------------------------
871 promote_lineitem_holds($mgr, $li) or return 0;
873 my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
875 # -----------------------------------------------------------------
876 # for each lineitem_detail, create the volume if necessary, create
877 # a copy, and link them all together.
878 # -----------------------------------------------------------------
880 for my $lid_id (@{$li_details}) {
882 my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
883 next if $lid->eg_copy_id;
885 # use the same callnumber label for all items within this lineitem
886 $lid->cn_label($first_cn) if $first_cn and not $lid->cn_label;
888 # apply defaults if necessary
889 return 0 unless complete_lineitem_detail($mgr, $lid);
891 $first_cn = $lid->cn_label unless $first_cn;
893 my $org = $lid->owning_lib;
894 my $label = $lid->cn_label;
895 my $bibid = $li->eg_bib_id;
897 my $volume = $mgr->cache($org, "cn.$bibid.$label");
899 $volume = create_volume($mgr, $li, $lid) or return 0;
900 $mgr->cache($org, "cn.$bibid.$label", $volume);
902 create_copy($mgr, $volume, $lid) or return 0;
905 return { li => $li, new_bib => $new_bib };
911 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
916 1, # override tcn collisions
919 if($U->event_code($record)) {
920 $mgr->editor->event($record);
921 $mgr->editor->rollback;
925 $li->eg_bib_id($record->id);
927 return update_lineitem($mgr, $li);
931 my($mgr, $li, $lid) = @_;
934 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
942 $mgr->editor->event($evt);
950 my($mgr, $volume, $lid) = @_;
951 my $copy = Fieldmapper::asset::copy->new;
953 $copy->loan_duration(2);
954 $copy->fine_level(2);
955 $copy->status(OILS_COPY_STATUS_ON_ORDER);
956 $copy->barcode($lid->barcode);
957 $copy->location($lid->location);
958 $copy->call_number($volume->id);
959 $copy->circ_lib($volume->owning_lib);
960 $copy->circ_modifier($lid->circ_modifier);
962 my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
964 $mgr->editor->event($evt);
969 $lid->eg_copy_id($copy->id);
970 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
978 # ----------------------------------------------------------------------------
979 # Workflow: Build a selection list from a Z39.50 search
980 # ----------------------------------------------------------------------------
982 __PACKAGE__->register_method(
984 api_name => 'open-ils.acq.picklist.search.z3950',
987 desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
989 {desc => 'Authentication token', type => 'string'},
990 {desc => 'Search definition', type => 'object'},
991 {desc => 'Picklist name, optional', type => 'string'},
997 my($self, $conn, $auth, $search, $name, $options) = @_;
998 my $e = new_editor(authtoken=>$auth);
999 return $e->event unless $e->checkauth;
1000 return $e->event unless $e->allowed('CREATE_PICKLIST');
1002 $search->{limit} ||= 10;
1005 my $ses = OpenSRF::AppSession->create('open-ils.search');
1006 my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
1011 while(my $resp = $req->recv(timeout=>60)) {
1014 my $e = new_editor(requestor=>$e->requestor, xact=>1);
1015 $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1016 $picklist = zsearch_build_pl($mgr, $name);
1020 my $result = $resp->content;
1021 my $count = $result->{count} || 0;
1022 $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
1024 for my $rec (@{$result->{records}}) {
1026 my $li = create_lineitem($mgr,
1027 picklist => $picklist->id,
1028 source_label => $result->{service},
1029 marc => $rec->{marcxml},
1030 eg_bib_id => $rec->{bibid}
1033 if($$options{respond_li}) {
1034 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
1035 if $$options{flesh_attrs};
1036 $li->clear_marc if $$options{clear_marc};
1037 $mgr->respond(lineitem => $li);
1044 $mgr->editor->commit;
1045 return $mgr->respond_complete;
1048 sub zsearch_build_pl {
1049 my($mgr, $name) = @_;
1052 my $picklist = $mgr->editor->search_acq_picklist({
1053 owner => $mgr->editor->requestor->id,
1057 if($name eq '' and $picklist) {
1058 return 0 unless delete_picklist($mgr, $picklist);
1062 return update_picklist($mgr, $picklist) if $picklist;
1063 return create_picklist($mgr, name => $name);
1067 # ----------------------------------------------------------------------------
1068 # Workflow: Build a selection list / PO by importing a batch of MARC records
1069 # ----------------------------------------------------------------------------
1071 __PACKAGE__->register_method(
1072 method => 'upload_records',
1073 api_name => 'open-ils.acq.process_upload_records',
1077 sub upload_records {
1078 my($self, $conn, $auth, $key) = @_;
1080 my $e = new_editor(authtoken => $auth, xact => 1);
1081 return $e->die_event unless $e->checkauth;
1082 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1084 my $cache = OpenSRF::Utils::Cache->new;
1086 my $data = $cache->get_cache("vandelay_import_spool_$key");
1087 my $purpose = $data->{purpose};
1088 my $filename = $data->{path};
1089 my $provider = $data->{provider};
1090 my $picklist = $data->{picklist};
1091 my $create_po = $data->{create_po};
1092 my $ordering_agency = $data->{ordering_agency};
1093 my $create_assets = $data->{create_assets};
1097 unless(-r $filename) {
1098 $logger->error("unable to read MARC file $filename");
1100 return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
1103 $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
1106 $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
1107 if($picklist->owner != $e->requestor->id) {
1108 return $e->die_event unless
1109 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
1111 $mgr->picklist($picklist);
1116 $po = create_purchase_order($mgr,
1117 ordering_agency => $ordering_agency,
1118 provider => $provider->id,
1120 ) or return $mgr->editor->die_event;
1123 $logger->info("acq processing MARC file=$filename");
1125 my $marctype = 'USMARC'; # ?
1126 my $batch = new MARC::Batch ($marctype, $filename);
1141 } catch Error with {
1143 $logger->warn("Proccessing of record $count in set $key failed with error $err. Skipping this record");
1150 ($xml = $r->as_xml_record()) =~ s/\n//sog;
1151 $xml =~ s/^<\?xml.+\?\s*>//go;
1152 $xml =~ s/>\s+</></go;
1153 $xml =~ s/\p{Cc}//go;
1154 $xml = $U->entityize($xml);
1155 $xml =~ s/[\x00-\x1f]//go;
1157 } catch Error with {
1159 $logger->warn("Proccessing XML of record $count in set $key failed with error $err. Skipping this record");
1162 next if $err or not $xml;
1165 source_label => $provider->code,
1166 provider => $provider->id,
1170 $args{picklist} = $picklist->id if $picklist;
1172 $args{purchase_order} = $po->id;
1173 $args{state} = 'on-order';
1176 my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
1178 $li->provider($provider); # flesh it, we'll need it later
1180 import_lineitem_details($mgr, $ordering_agency, $li) or return $mgr->editor->die_event;
1183 push(@li_list, $li->id);
1189 $cache->delete_cache('vandelay_import_spool_' . $key);
1191 if($create_assets) {
1192 create_lineitem_list_assets($mgr, \@li_list) or return $e->die_event;
1195 return $mgr->respond_complete;
1198 sub import_lineitem_details {
1199 my($mgr, $ordering_agency, $li) = @_;
1201 my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
1202 return 1 unless @$holdings;
1203 my $org_path = $U->get_org_ancestors($ordering_agency);
1204 $org_path = [ reverse (@$org_path) ];
1209 # create a lineitem detail for each copy in the data
1211 my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx);
1212 last unless defined $compiled;
1213 return 0 unless $compiled;
1215 # this takes the price of the last copy and uses it as the lineitem price
1216 # need to determine if a given record would include different prices for the same item
1217 $price = $$compiled{price};
1219 for(1..$$compiled{quantity}) {
1220 my $lid = create_lineitem_detail($mgr,
1221 lineitem => $li->id,
1222 owning_lib => $$compiled{owning_lib},
1223 cn_label => $$compiled{call_number},
1224 fund => $$compiled{fund},
1225 circ_modifier => $$compiled{circ_modifier},
1226 note => $$compiled{note},
1227 location => $$compiled{copy_location},
1228 collection_code => $$compiled{collection_code}
1236 # set the price attr so we'll know the source of the price
1239 attr_name => 'estimated_price',
1240 attr_type => 'lineitem_local_attr_definition',
1241 attr_value => $price,
1245 # if we're creating a purchase order, create the debits
1246 if($li->purchase_order) {
1247 create_lineitem_debits($mgr, $li, $price, 2) or return 0;
1254 # return hash on success, 0 on error, undef on no more holdings
1255 sub extract_lineitem_detail_data {
1256 my($mgr, $org_path, $holdings, $index) = @_;
1258 my @data_list = grep { $_->{holding} eq $index } @$holdings;
1259 return undef unless @data_list;
1261 my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1262 my $base_org = $$org_path[0];
1266 $logger->error("Item import extraction error: $msg");
1267 $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1268 $mgr->editor->rollback;
1269 $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1273 $compiled{quantity} ||= 1;
1275 # ---------------------------------------------------------------------
1277 my $code = $compiled{fund_code};
1278 return $killme->('no fund code provided') unless $code;
1280 my $fund = $mgr->cache($base_org, "fund.$code");
1282 # search up the org tree for the most appropriate fund
1283 for my $org (@$org_path) {
1284 $fund = $mgr->editor->search_acq_fund(
1285 {org => $org, code => $code, year => DateTime->now->year}, {idlist => 1})->[0];
1289 return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1290 $compiled{fund} = $fund;
1291 $mgr->cache($base_org, "fund.$code", $fund);
1294 # ---------------------------------------------------------------------
1296 my $sn = $compiled{owning_lib};
1297 return $killme->('no owning_lib defined') unless $sn;
1299 $mgr->cache($base_org, "orgsn.$sn") ||
1300 $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1301 return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1302 $compiled{owning_lib} = $org_id;
1303 $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1306 # ---------------------------------------------------------------------
1309 $code = $compiled{circ_modifier};
1313 $mod = $mgr->cache($base_org, "mod.$code") ||
1314 $mgr->editor->retrieve_config_circ_modifier($code);
1315 return $killme->("invlalid circ_modifier $code") unless $mod;
1316 $mgr->cache($base_org, "mod.$code", $mod);
1320 $mod = get_default_circ_modifier($mgr, $base_org)
1321 or return $killme->('no circ_modifier defined');
1324 $compiled{circ_modifier} = $mod;
1327 # ---------------------------------------------------------------------
1329 my $name = $compiled{copy_location};
1331 my $loc = $mgr->cache($base_org, "copy_loc.$name");
1333 for my $org (@$org_path) {
1334 $loc = $mgr->editor->search_asset_copy_location(
1335 {owning_lib => $org, name => $name}, {idlist => 1})->[0];
1339 return $killme->("Invalid copy location $name") unless $loc;
1340 $compiled{copy_location} = $loc;
1341 $mgr->cache($base_org, "copy_loc.$name", $loc);
1349 # ----------------------------------------------------------------------------
1350 # Workflow: Given an existing purchase order, import/create the bibs,
1351 # callnumber and copy objects
1352 # ----------------------------------------------------------------------------
1354 __PACKAGE__->register_method(
1355 method => 'create_po_assets',
1356 api_name => 'open-ils.acq.purchase_order.assets.create',
1358 desc => q/Creates assets for each lineitem in the purchase order/,
1360 {desc => 'Authentication token', type => 'string'},
1361 {desc => 'The purchase order id', type => 'number'},
1363 return => {desc => 'Streams a total versus completed counts object, event on error'}
1367 sub create_po_assets {
1368 my($self, $conn, $auth, $po_id) = @_;
1370 my $e = new_editor(authtoken=>$auth, xact=>1);
1371 return $e->die_event unless $e->checkauth;
1372 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1374 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1376 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1378 # it's ugly, but it's fast. Get the total count of lineitem detail objects to process
1379 my $lid_total = $e->json_query({
1380 select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] },
1386 join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1390 where => {'+acqpo' => {id => $po_id}}
1393 $mgr->total(scalar(@$li_ids) + $lid_total);
1395 create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1398 update_purchase_order($mgr, $po) or return $e->die_event;
1401 return $mgr->respond_complete;
1406 __PACKAGE__->register_method(
1407 method => 'create_purchase_order_api',
1408 api_name => 'open-ils.acq.purchase_order.create',
1410 desc => 'Creates a new purchase order',
1412 {desc => 'Authentication token', type => 'string'},
1413 {desc => 'purchase_order to create', type => 'object'}
1415 return => {desc => 'The purchase order id, Event on failure'}
1419 sub create_purchase_order_api {
1420 my($self, $conn, $auth, $po, $args) = @_;
1423 my $e = new_editor(xact=>1, authtoken=>$auth);
1424 return $e->die_event unless $e->checkauth;
1425 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1426 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1429 my %pargs = (ordering_agency => $e->requestor->ws_ou); # default
1430 $pargs{provider} = $po->provider if $po->provider;
1431 $pargs{ordering_agency} = $po->ordering_agency if $po->ordering_agency;
1432 $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1434 my $li_ids = $$args{lineitems};
1438 for my $li_id (@$li_ids) {
1440 my $li = $e->retrieve_acq_lineitem([
1442 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1443 ]) or return $e->die_event;
1445 $li->provider($po->provider);
1446 $li->purchase_order($po->id);
1447 $li->state('pending-order');
1448 update_lineitem($mgr, $li) or return $e->die_event;
1451 create_lineitem_debits($mgr, $li) or return $e->die_event;
1455 # commit before starting the asset creation
1458 if($li_ids and $$args{create_assets}) {
1459 create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1462 return $mgr->respond_complete;
1466 __PACKAGE__->register_method(
1467 method => 'lineitem_detail_CUD_batch',
1468 api_name => 'open-ils.acq.lineitem_detail.cud.batch',
1471 desc => q/Creates a new purchase order line item detail.
1472 Additionally creates the associated fund_debit/,
1474 {desc => 'Authentication token', type => 'string'},
1475 {desc => 'List of lineitem_details to create', type => 'array'},
1477 return => {desc => 'Streaming response of current position in the array'}
1481 sub lineitem_detail_CUD_batch {
1482 my($self, $conn, $auth, $li_details) = @_;
1484 my $e = new_editor(xact=>1, authtoken=>$auth);
1485 return $e->die_event unless $e->checkauth;
1486 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1490 $mgr->total(scalar(@$li_details));
1494 for my $lid (@$li_details) {
1496 my $li = $li_cache{$lid->lineitem} || $e->retrieve_acq_lineitem($lid->lineitem);
1499 create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
1501 } elsif($lid->ischanged) {
1502 $e->update_acq_lineitem_detail($lid) or return $e->die_event;
1504 } elsif($lid->isdeleted) {
1505 delete_lineitem_detail($mgr, $lid) or return $e->die_event;
1508 $mgr->respond(li => $li);
1509 $li_cache{$lid->lineitem} = $li;
1513 return $mgr->respond_complete;
1517 __PACKAGE__->register_method(
1518 method => 'receive_po_api',
1519 api_name => 'open-ils.acq.purchase_order.receive'
1522 sub receive_po_api {
1523 my($self, $conn, $auth, $po_id) = @_;
1524 my $e = new_editor(xact => 1, authtoken => $auth);
1525 return $e->die_event unless $e->checkauth;
1526 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1528 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1529 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1531 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1533 for my $li_id (@$li_ids) {
1534 receive_lineitem($mgr, $li_id) or return $e->die_event;
1538 $po->state('received');
1539 update_purchase_order($mgr, $po) or return $e->die_event;
1542 return $mgr->respond_complete;
1546 __PACKAGE__->register_method(
1547 method => 'receive_lineitem_detail_api',
1548 api_name => 'open-ils.acq.lineitem_detail.receive',
1550 desc => 'Mark a lineitem_detail as received',
1552 {desc => 'Authentication token', type => 'string'},
1553 {desc => 'lineitem detail ID', type => 'number'}
1555 return => {desc => '1 on success, Event on error'}
1559 sub receive_lineitem_detail_api {
1560 my($self, $conn, $auth, $lid_id) = @_;
1562 my $e = new_editor(xact=>1, authtoken=>$auth);
1563 return $e->die_event unless $e->checkauth;
1564 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1566 my $lid = $e->retrieve_acq_lineitem_detail([
1570 acqlid => ['lineitem'],
1571 jub => ['purchase_order']
1576 return $e->die_event unless $e->allowed(
1577 'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
1579 receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
1584 __PACKAGE__->register_method(
1585 method => 'receive_lineitem_api',
1586 api_name => 'open-ils.acq.lineitem.receive',
1588 desc => 'Mark a lineitem as received',
1590 {desc => 'Authentication token', type => 'string'},
1591 {desc => 'lineitem detail ID', type => 'number'}
1593 return => {desc => '1 on success, Event on error'}
1597 sub receive_lineitem_api {
1598 my($self, $conn, $auth, $li_id) = @_;
1600 my $e = new_editor(xact=>1, authtoken=>$auth);
1601 return $e->die_event unless $e->checkauth;
1602 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1604 my $li = $e->retrieve_acq_lineitem([
1608 jub => ['purchase_order']
1611 ]) or return $e->die_event;
1613 return $e->die_event unless $e->allowed(
1614 'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
1616 receive_lineitem($mgr, $li_id) or return $e->die_event;
1619 $conn->respond_complete(1);
1620 $mgr->run_post_response_hooks;
1625 __PACKAGE__->register_method(
1626 method => 'rollback_receive_po_api',
1627 api_name => 'open-ils.acq.purchase_order.receive.rollback'
1630 sub rollback_receive_po_api {
1631 my($self, $conn, $auth, $po_id) = @_;
1632 my $e = new_editor(xact => 1, authtoken => $auth);
1633 return $e->die_event unless $e->checkauth;
1634 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1636 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1637 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1639 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1641 for my $li_id (@$li_ids) {
1642 rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
1646 $po->state('on-order');
1647 update_purchase_order($mgr, $po) or return $e->die_event;
1650 return $mgr->respond_complete;
1654 __PACKAGE__->register_method(
1655 method => 'rollback_receive_lineitem_detail_api',
1656 api_name => 'open-ils.acq.lineitem_detail.receive.rollback',
1658 desc => 'Mark a lineitem_detail as received',
1660 {desc => 'Authentication token', type => 'string'},
1661 {desc => 'lineitem detail ID', type => 'number'}
1663 return => {desc => '1 on success, Event on error'}
1667 sub rollback_receive_lineitem_detail_api {
1668 my($self, $conn, $auth, $lid_id) = @_;
1670 my $e = new_editor(xact=>1, authtoken=>$auth);
1671 return $e->die_event unless $e->checkauth;
1672 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1674 my $lid = $e->retrieve_acq_lineitem_detail([
1678 acqlid => ['lineitem'],
1679 jub => ['purchase_order']
1683 my $li = $lid->lineitem;
1684 my $po = $li->purchase_order;
1686 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1687 rollback_receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
1689 $li->state('on-order');
1690 $po->state('on-order');
1691 udpate_lineitem($mgr, $li) or return $e->die_event;
1692 udpate_purchase_order($mgr, $po) or return $e->die_event;
1698 __PACKAGE__->register_method(
1699 method => 'rollback_receive_lineitem_api',
1700 api_name => 'open-ils.acq.lineitem.receive.rollback',
1702 desc => 'Mark a lineitem as received',
1704 {desc => 'Authentication token', type => 'string'},
1705 {desc => 'lineitem detail ID', type => 'number'}
1707 return => {desc => '1 on success, Event on error'}
1711 sub rollback_receive_lineitem_api {
1712 my($self, $conn, $auth, $li_id) = @_;
1714 my $e = new_editor(xact=>1, authtoken=>$auth);
1715 return $e->die_event unless $e->checkauth;
1716 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1718 my $li = $e->retrieve_acq_lineitem_detail([
1722 jub => ['purchase_order']
1726 my $po = $li->purchase_order;
1728 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1730 rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
1732 $po->state('on-order');
1733 update_purchase_order($mgr, $po) or return $e->die_event;
1740 __PACKAGE__->register_method(
1741 method => 'set_lineitem_price_api',
1742 api_name => 'open-ils.acq.lineitem.price.set',
1744 desc => 'Set lineitem price. If debits already exist, update them as well',
1746 {desc => 'Authentication token', type => 'string'},
1747 {desc => 'lineitem ID', type => 'number'}
1749 return => {desc => 'status blob, Event on error'}
1753 sub set_lineitem_price_api {
1754 my($self, $conn, $auth, $li_id, $price, $currency) = @_;
1756 my $e = new_editor(xact=>1, authtoken=>$auth);
1757 return $e->die_event unless $e->checkauth;
1758 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1762 my $li = $e->retrieve_acq_lineitem($li_id) or return $e->die_event;
1764 # update the local attr for estimated price
1767 attr_name => 'estimated_price',
1768 attr_type => 'lineitem_local_attr_definition',
1769 attr_value => $price,
1771 ) or return $e->die_event;
1773 my $lid_ids = $e->search_acq_lineitem_detail(
1774 {lineitem => $li_id, fund_debit => {'!=' => undef}},
1778 for my $lid_id (@$lid_ids) {
1780 my $lid = $e->retrieve_acq_lineitem_detail([
1782 flesh => 1, flesh_fields => {acqlid => ['fund', 'fund_debit']}}
1785 # onless otherwise specified, assume currency of new price is same as currency type of the fund
1786 $currency ||= $lid->fund->currency_type;
1787 my $amount = $price;
1789 if($lid->fund->currency_type ne $currency) {
1790 $amount = currency_conversion($mgr, $currency, $lid->fund->currency_type, $price);
1793 $lid->fund_debit->origin_currency_type($currency);
1794 $lid->fund_debit->origin_amount($price);
1795 $lid->fund_debit->amount($amount);
1797 $e->update_acq_fund_debit($lid->fund_debit) or return $e->die_event;
1803 return $mgr->respond_complete;
1807 __PACKAGE__->register_method(
1808 method => 'clone_picklist_api',
1809 api_name => 'open-ils.acq.picklist.clone',
1811 desc => 'Clones a picklist, including lineitem and lineitem details',
1813 {desc => 'Authentication token', type => 'string'},
1814 {desc => 'Picklist ID', type => 'number'},
1815 {desc => 'New Picklist Name', type => 'string'}
1817 return => {desc => 'status blob, Event on error'}
1821 sub clone_picklist_api {
1822 my($self, $conn, $auth, $pl_id, $name) = @_;
1824 my $e = new_editor(xact=>1, authtoken=>$auth);
1825 return $e->die_event unless $e->checkauth;
1826 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1828 my $old_pl = $e->retrieve_acq_picklist($pl_id);
1829 my $new_pl = create_picklist($mgr, %{$old_pl->to_bare_hash}, name => $name) or return $e->die_event;
1831 my $li_ids = $e->search_acq_lineitem({picklist => $pl_id}, {idlist => 1});
1833 for my $li_id (@$li_ids) {
1835 # copy the lineitems
1836 my $li = $e->retrieve_acq_lineitem($li_id);
1837 my $new_li = create_lineitem($mgr, %{$li->to_bare_hash}, picklist => $new_pl->id) or return $e->die_event;
1839 my $lid_ids = $e->search_acq_lineitem_detail({lineitem => $li_id}, {idlist => 1});
1840 for my $lid_id (@$lid_ids) {
1842 # copy the lineitem details
1843 my $lid = $e->retrieve_acq_lineitem_detail($lid_id);
1844 create_lineitem_detail($mgr, %{$lid->to_bare_hash}, lineitem => $new_li->id) or return $e->die_event;
1851 return $mgr->respond_complete;
1855 __PACKAGE__->register_method(
1856 method => 'merge_picklist_api',
1857 api_name => 'open-ils.acq.picklist.merge',
1859 desc => 'Merges 2 or more picklists into a single list',
1861 {desc => 'Authentication token', type => 'string'},
1862 {desc => 'Lead Picklist ID', type => 'number'},
1863 {desc => 'List of subordinate picklist IDs', type => 'array'}
1865 return => {desc => 'status blob, Event on error'}
1869 sub merge_picklist_api {
1870 my($self, $conn, $auth, $lead_pl, $pl_list) = @_;
1872 my $e = new_editor(xact=>1, authtoken=>$auth);
1873 return $e->die_event unless $e->checkauth;
1874 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1876 # XXX perms on each picklist modified
1878 # point all of the lineitems at the lead picklist
1879 my $li_ids = $e->search_acq_lineitem({picklist => $pl_list}, {idlist => 1});
1881 for my $li_id (@$li_ids) {
1882 my $li = $e->retrieve_acq_lineitem($li_id);
1883 $li->picklist($lead_pl);
1884 update_lineitem($mgr, $li) or return $e->die_event;
1888 # now delete the subordinate lists
1889 for my $pl_id (@$pl_list) {
1890 my $pl = $e->retrieve_acq_picklist($pl_id);
1891 $e->delete_acq_picklist($pl) or return $e->die_event;
1895 return $mgr->respond_complete;
1899 __PACKAGE__->register_method(
1900 method => 'delete_picklist_api',
1901 api_name => 'open-ils.acq.picklist.delete',
1903 desc => q/Deletes a picklist. It also deletes any lineitems in the "new" state.
1904 Other attached lineitems are detached'/,
1906 {desc => 'Authentication token', type => 'string'},
1907 {desc => 'Picklist ID to delete', type => 'number'}
1909 return => {desc => '1 on success, Event on error'}
1913 sub delete_picklist_api {
1914 my($self, $conn, $auth, $picklist_id) = @_;
1915 my $e = new_editor(xact=>1, authtoken=>$auth);
1916 return $e->die_event unless $e->checkauth;
1917 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1918 my $pl = $e->retrieve_acq_picklist($picklist_id) or return $e->die_event;
1919 delete_picklist($mgr, $pl) or return $e->die_event;
1921 return $mgr->respond_complete;
1926 __PACKAGE__->register_method(
1927 method => 'activate_purchase_order',
1928 api_name => 'open-ils.acq.purchase_order.activate',
1930 desc => q/Activates a purchase order. This updates the status of the PO
1931 and Lineitems to 'on-order'. Activated PO's are ready for EDI delivery
1934 {desc => 'Authentication token', type => 'string'},
1935 {desc => 'Purchase ID', type => 'number'}
1937 return => {desc => '1 on success, Event on error'}
1941 sub activate_purchase_order {
1942 my($self, $conn, $auth, $po_id) = @_;
1943 my $e = new_editor(xact=>1, authtoken=>$auth);
1944 return $e->die_event unless $e->checkauth;
1945 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1947 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1948 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1950 $po->state('on-order');
1951 update_purchase_order($mgr, $po) or return $e->die_event;
1954 {purchase_order => $po_id, state => 'pending-order'},
1958 while( my $li = $e->search_acq_lineitem($query)->[0] ) {
1959 $li->state('on-order');
1960 update_lineitem($mgr, $li) or return $e->die_event;
1961 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li->id, 'aur.ordered'); });
1966 $conn->respond_complete(1);
1967 $mgr->run_post_response_hooks;
1972 __PACKAGE__->register_method(
1973 method => 'split_purchase_order_by_lineitems',
1974 api_name => 'open-ils.acq.purchase_order.split_by_lineitems',
1976 desc => q/Splits a PO into many POs, 1 per lineitem. Only works for
1977 POs a) with more than one lineitems, and b) in the "pending" state./,
1979 {desc => 'Authentication token', type => 'string'},
1980 {desc => 'Purchase order ID', type => 'number'}
1982 return => {desc => 'list of new PO IDs on success, Event on error'}
1986 sub split_purchase_order_by_lineitems {
1987 my ($self, $conn, $auth, $po_id) = @_;
1989 my $e = new_editor("xact" => 1, "authtoken" => $auth);
1990 return $e->die_event unless $e->checkauth;
1992 my $po = $e->retrieve_acq_purchase_order([
1995 "flesh_fields" => {"acqpo" => [qw/lineitems notes/]}
1997 ]) or return $e->die_event;
1999 return $e->die_event
2000 unless $e->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2002 unless ($po->state eq "pending") {
2004 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_LATE");
2007 unless (@{$po->lineitems} > 1) {
2009 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_SHORT");
2012 # To split an existing PO into many, it seems unwise to just delete the
2013 # original PO, so we'll instead detach all of the original POs' lineitems
2014 # but the first, then create new POs for each of the remaining LIs, and
2015 # then attach the LIs to their new POs.
2017 my @po_ids = ($po->id);
2018 my @moving_li = @{$po->lineitems};
2019 shift @moving_li; # discard first LI
2021 foreach my $li (@moving_li) {
2022 my $new_po = $po->clone;
2024 $new_po->clear_name;
2025 $new_po->creator($e->requestor->id);
2026 $new_po->editor($e->requestor->id);
2027 $new_po->owner($e->requestor->id);
2028 $new_po->edit_time("now");
2029 $new_po->create_time("now");
2031 $new_po = $e->create_acq_purchase_order($new_po);
2033 # Clone any notes attached to the old PO and attach to the new one.
2034 foreach my $note (@{$po->notes}) {
2035 my $new_note = $note->clone;
2036 $new_note->clear_id;
2037 $new_note->edit_time("now");
2038 $new_note->purchase_order($new_po->id);
2039 $e->create_acq_po_note($new_note);
2042 $li->edit_time("now");
2043 $li->purchase_order($new_po->id);
2044 $e->update_acq_lineitem($li);
2046 push @po_ids, $new_po->id;
2049 $po->edit_time("now");
2050 $e->update_acq_purchase_order($po);
2052 return \@po_ids if $e->commit;
2053 return $e->die_event;
2057 __PACKAGE__->register_method(
2058 method => 'cancel_lineitem_api',
2059 api_name => 'open-ils.acq.lineitem.cancel',
2061 desc => q/Cancels an on-order lineitem/,
2063 {desc => 'Authentication token', type => 'string'},
2064 {desc => 'Lineitem ID to cancel', type => 'number'},
2065 {desc => 'Cancel Cause ID', type => 'number'}
2067 return => {desc => '1 on success, Event on error'}
2071 sub cancel_lineitem_api {
2072 my($self, $conn, $auth, $li_id, $cancel_cause) = @_;
2074 my $e = new_editor(xact=>1, authtoken=>$auth);
2075 return $e->die_event unless $e->checkauth;
2076 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2078 my $li = $e->retrieve_acq_lineitem([$li_id,
2079 {flesh => 1, flesh_fields => {jub => [q/purchase_order/]}}]);
2081 unless( $li->purchase_order and ($li->state eq 'on-order' or $li->state eq 'pending-order') ) {
2083 return OpenILS::Event->new('BAD_PARAMS')
2086 return $e->die_event unless
2087 $e->allowed('CREATE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
2089 $li->state('cancelled');
2091 # TODO delete the associated fund debits?
2092 # TODO add support for cancel reasons
2093 # TODO who/what/where/how do we indicate this change for electronic orders?
2095 update_lineitem($mgr, $li) or return $e->die_event;
2098 $conn->respond_complete($li);
2099 create_lineitem_status_events($mgr, $li_id, 'aur.cancelled');