1 package OpenILS::Application::Acq::BatchManager;
2 use OpenILS::Application::Acq::Financials;
3 use OpenSRF::AppSession;
4 use OpenSRF::EX qw/:try/;
5 use strict; use warnings;
7 # empirically derived number of responses we can
8 # stream back before the XUL client has indigestion
9 use constant MAX_RESPONSES => 20;
12 my($class, %args) = @_;
13 my $self = bless(\%args, $class);
22 purchase_order => undef,
30 $self->throttle(4) unless $self->throttle;
31 $self->exponential_falloff(1) unless $self->exponential_falloff;
32 $self->{post_proc_queue} = [];
33 $self->{last_respond_progress} = 0;
39 $self->{conn} = $val if $val;
44 $self->{throttle} = $val if $val;
45 return $self->{throttle};
47 sub exponential_falloff {
49 $self->{exponential_falloff} = $val if defined $val;
50 return $self->{exponential_falloff};
53 my($self, %other_args) = @_;
54 if($self->throttle and not %other_args) {
56 ($self->{args}->{progress} - $self->{last_respond_progress}) >= $self->throttle
59 $self->conn->respond({ %{$self->{args}}, %other_args });
60 $self->{last_respond_progress} = $self->{args}->{progress};
61 $self->throttle($self->throttle * 2) if ($self->exponential_falloff() and $self->throttle < 256);
63 sub respond_complete {
64 my($self, %other_args) = @_;
66 $self->conn->respond_complete({ %{$self->{args}}, %other_args });
67 $self->run_post_response_hooks;
71 # run the post response hook subs, shifting them off as we go
72 sub run_post_response_hooks {
74 (shift @{$self->{post_proc_queue}})->() while @{$self->{post_proc_queue}};
77 # any subs passed to this method will be run after the call to respond_complete
80 push(@{$self->{post_proc_queue}}, $sub);
85 $self->{args}->{total} = $val if defined $val;
86 $self->{args}->{maximum} = $self->{args}->{total};
87 if ($self->{args}->{maximum}) {
88 # if a total has been set, space responses linearly
89 $self->exponential_falloff(0);
90 $self->throttle(int($self->{args}->{maximum} / MAX_RESPONSES));
91 $self->throttle(4) if $self->throttle < 4;
93 return $self->{args}->{total};
97 $self->{args}->{purchase_order} = $val if $val;
101 my($self, $val) = @_;
102 $self->{args}->{picklist} = $val if $val;
107 $self->{args}->{lid} += 1;
108 $self->{args}->{progress} += 1;
113 $self->{args}->{li} += 1;
114 $self->{args}->{progress} += 1;
119 $self->{args}->{vqbr} += 1;
120 $self->{args}->{progress} += 1;
125 $self->{args}->{copies} += 1;
126 $self->{args}->{progress} += 1;
131 $self->{args}->{bibs} += 1;
132 $self->{args}->{progress} += 1;
136 my($self, $amount) = @_;
137 $self->{args}->{debits_accrued} += $amount;
138 $self->{args}->{progress} += 1;
142 my($self, $editor) = @_;
143 $self->{editor} = $editor if defined $editor;
144 return $self->{editor};
148 $self->{args}->{complete} = 1;
153 my($self, $org, $key, $val) = @_;
154 $self->{cache}->{$org} = {} unless $self->{cache}->{org};
155 $self->{cache}->{$org}->{$key} = $val if defined $val;
156 return $self->{cache}->{$org}->{$key};
160 package OpenILS::Application::Acq::Order;
161 use base qw/OpenILS::Application/;
162 use strict; use warnings;
163 # ----------------------------------------------------------------------------
164 # Break up each component of the order process and pieces into managable
165 # actions that can be shared across different workflows
166 # ----------------------------------------------------------------------------
168 use OpenSRF::Utils::Logger qw(:logger);
169 use OpenSRF::Utils::JSON;
170 use OpenSRF::AppSession;
171 use OpenILS::Utils::Fieldmapper;
172 use OpenILS::Utils::CStoreEditor q/:funcs/;
173 use OpenILS::Utils::Normalize qw/clean_marc/;
174 use OpenILS::Const qw/:const/;
175 use OpenSRF::EX q/:try/;
176 use OpenILS::Application::AppUtils;
177 use OpenILS::Application::Cat::BibCommon;
178 use OpenILS::Application::Cat::AssetCommon;
181 use MARC::File::XML (BinaryEncoding => 'UTF-8');
182 use Digest::MD5 qw(md5_hex);
184 $Data::Dumper::Indent = 0;
185 my $U = 'OpenILS::Application::AppUtils';
188 # ----------------------------------------------------------------------------
190 # ----------------------------------------------------------------------------
191 sub create_lineitem {
192 my($mgr, %args) = @_;
193 my $li = Fieldmapper::acq::lineitem->new;
194 $li->creator($mgr->editor->requestor->id);
195 $li->selector($li->creator);
196 $li->editor($li->creator);
197 $li->create_time('now');
198 $li->edit_time('now');
200 $li->$_($args{$_}) for keys %args;
203 $mgr->editor->create_acq_lineitem($li) or return 0;
205 unless($li->estimated_unit_price) {
206 # extract the price from the MARC data
207 my $price = get_li_price_from_attr($mgr->editor, $li) or return $li;
208 $li->estimated_unit_price($price);
209 return update_lineitem($mgr, $li);
215 sub get_li_price_from_attr {
217 my $attrs = $li->attributes || $e->search_acq_lineitem_attr({lineitem => $li->id});
219 for my $attr_type (qw/
220 lineitem_local_attr_definition
221 lineitem_prov_attr_definition
222 lineitem_marc_attr_definition/) {
225 $_->attr_name eq 'estimated_price' and
226 $_->attr_type eq $attr_type } @$attrs;
228 return $attr->attr_value if $attr;
235 sub update_lineitem {
237 $li->edit_time('now');
238 $li->editor($mgr->editor->requestor->id);
240 return $mgr->editor->retrieve_acq_lineitem($mgr->editor->data) if
241 $mgr->editor->update_acq_lineitem($li);
246 # ----------------------------------------------------------------------------
247 # Create real holds from patron requests for a given lineitem
248 # ----------------------------------------------------------------------------
249 sub promote_lineitem_holds {
252 my $requests = $mgr->editor->search_acq_user_request(
253 { lineitem => $li->id,
255 [ { need_before => {'>' => 'now'} },
256 { need_before => undef }
261 for my $request ( @$requests ) {
263 $request->eg_bib( $li->eg_bib_id );
264 $mgr->editor->update_acq_user_request( $request ) or return 0;
266 next unless ($U->is_true( $request->hold ));
268 my $existing_hold = $mgr->editor->search_action_hold_request(
269 {acq_request => $request->id})->[0];
270 if ($existing_hold) {
271 $logger->warn("Existing hold found where acq_request = $request->id");
274 if (! $li->eg_bib_id) {
275 $logger->error("Hold creation attempt for aur $request->id where li.eg_bib_id is null");
279 my $hold = Fieldmapper::action::hold_request->new;
280 $hold->usr( $request->usr );
281 $hold->requestor( $request->usr );
282 $hold->request_time( $request->request_date );
283 $hold->pickup_lib( $request->pickup_lib );
284 $hold->request_lib( $request->pickup_lib );
285 $hold->selection_ou( $request->pickup_lib );
286 $hold->phone_notify( $request->phone_notify );
287 $hold->email_notify( $request->email_notify );
288 $hold->expire_time( $request->need_before );
289 $hold->acq_request( $request->id );
291 if ($request->holdable_formats) {
292 my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
294 $hold->hold_type( 'M' );
295 $hold->holdable_formats( $request->holdable_formats );
296 $hold->target( $mrm->metarecord );
300 if (!$hold->target) {
301 $hold->hold_type( 'T' );
302 $hold->target( $li->eg_bib_id );
305 # if behind-the-desk holds are supported at the
306 # pickup library, apply the patron default
307 my $bdous = $U->ou_ancestor_setting_value(
309 'circ.holds.behind_desk_pickup_supported',
314 my $set = $mgr->editor->search_actor_user_setting(
315 {usr => $hold->usr, name => 'circ.holds_behind_desk'})->[0];
317 $hold->behind_desk('t') if $set and
318 OpenSRF::Utils::JSON->JSON2perl($set->value);
321 $mgr->editor->create_action_hold_request( $hold ) or return 0;
327 sub delete_lineitem {
329 $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
331 # delete the attached lineitem_details
332 my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
333 for my $lid_id (@$lid_ids) {
334 return 0 unless delete_lineitem_detail($mgr, $lid_id);
338 return $mgr->editor->delete_acq_lineitem($li);
341 # begins and commit transactions as it goes
342 # bib_only exits before creation of copies and callnumbers
343 sub create_lineitem_list_assets {
344 my($mgr, $li_ids, $vandelay, $bib_only) = @_;
346 # Do not create line items if none are specified
347 return {} unless (scalar(@$li_ids));
349 if (check_import_li_marc_perms($mgr, $li_ids)) { # event on error
350 $logger->error("acq-vl: user does not have permission to import acq records");
354 my $res = import_li_bibs_via_vandelay($mgr, $li_ids, $vandelay);
355 return undef unless $res;
356 return $res if $bib_only;
358 # create the bibs/volumes/copies for the successfully imported records
359 for my $li_id (@{$res->{li_ids}}) {
360 $mgr->editor->xact_begin;
361 my $data = create_lineitem_assets($mgr, $li_id) or return undef;
362 $mgr->editor->xact_commit;
369 sub test_vandelay_import_args {
370 my $vandelay = shift;
371 my $q_needed = shift;
373 # we need valid args and (sometimes) a queue
374 return 0 unless $vandelay and (
376 $vandelay->{queue_name} or
377 $vandelay->{existing_queue}
380 # match-based merge/overlay import
381 return 2 if $vandelay->{merge_profile} and (
382 $vandelay->{auto_overlay_exact} or
383 $vandelay->{auto_overlay_1match} or
384 $vandelay->{auto_overlay_best_match}
388 return 2 if $vandelay->{import_no_match};
390 return 1; # queue only
393 sub find_or_create_vandelay_queue {
394 my ($e, $vandelay) = @_;
397 if (my $name = $vandelay->{queue_name}) {
399 # first, see if a queue w/ this name already exists
400 # for this user. If so, use that instead.
402 $queue = $e->search_vandelay_bib_queue(
403 {name => $name, owner => $e->requestor->id})->[0];
407 $logger->info("acq-vl: using existing queue $name");
411 $logger->info("acq-vl: creating new vandelay queue $name");
413 $queue = new Fieldmapper::vandelay::bib_queue;
415 $queue->queue_type('acq');
416 $queue->owner($e->requestor->id);
417 $queue->match_set($vandelay->{match_set} || undef); # avoid ''
418 $queue = $e->create_vandelay_bib_queue($queue) or return undef;
422 $queue = $e->retrieve_vandelay_bib_queue($vandelay->{existing_queue})
430 sub import_li_bibs_via_vandelay {
431 my ($mgr, $li_ids, $vandelay) = @_;
432 my $res = {li_ids => []};
433 my $e = $mgr->editor;
436 my $needs_importing = $e->search_acq_lineitem(
437 {id => $li_ids, eg_bib_id => undef},
441 if (!@$needs_importing) {
442 $logger->info("acq-vl: all records already imported. no Vandelay work to do");
443 return {li_ids => $li_ids};
446 # see if we have any records that are not yet linked to VL records (i.e.
447 # not in a queue). This will tell us if lack of a queue name is an error.
448 my $non_queued = $e->search_acq_lineitem(
449 {id => $needs_importing, queued_record => undef},
453 # add the already-imported records to the response list
454 push(@{$res->{li_ids}}, grep { $_ != @$needs_importing } @$li_ids);
456 $logger->info("acq-vl: processing recs via Vandelay with args: ".Dumper($vandelay));
458 my $vl_stat = test_vandelay_import_args($vandelay, scalar(@$non_queued));
460 $logger->error("acq-vl: invalid vandelay arguments for acq import (queue needed)");
466 # when any non-queued lineitems exist, their vandelay counterparts
467 # require a place to live.
468 $queue = find_or_create_vandelay_queue($e, $vandelay) or return $res;
471 # if all lineitems are already queued, the queue reported to the user
472 # is purely for information / convenience. pick a random queue.
473 $queue = $e->retrieve_acq_lineitem([
474 $needs_importing->[0], {
477 jub => ['queued_record'],
481 ])->queued_record->queue;
484 $mgr->{args}->{queue} = $queue;
486 # load the lineitems into the queue for merge processing
489 for my $li_id (@$needs_importing) {
491 my $li = $e->retrieve_acq_lineitem($li_id) or return $res;
493 if ($li->queued_record) {
494 $logger->info("acq-vl: $li_id already linked to a vandelay record");
495 push(@vqbr_ids, $li->queued_record);
498 $logger->info("acq-vl: creating new vandelay record for lineitem $li_id");
500 # create a new VL queued record and link it up
501 my $vqbr = Fieldmapper::vandelay::queued_bib_record->new;
502 $vqbr->marc($li->marc);
503 $vqbr->queue($queue->id);
504 $vqbr->bib_source($vandelay->{bib_source} || undef); # avoid ''
505 $vqbr = $e->create_vandelay_queued_bib_record($vqbr) or return $res;
506 push(@vqbr_ids, $vqbr->id);
508 # tell the acq record which vandelay record it's linked to
509 $li->queued_record($vqbr->id);
510 $e->update_acq_lineitem($li) or return $res;
518 $logger->info("acq-vl: created vandelay records [@vqbr_ids]");
520 # we have to commit the transaction now since
521 # vandelay uses its own transactions.
524 return $res if $vl_stat == 1; # queue only
526 # Import the bibs via vandelay. Note: Vandely will
527 # update acq.lineitem.eg_bib_id on successful import.
529 $vandelay->{report_all} = 1;
530 my $ses = OpenSRF::AppSession->create('open-ils.vandelay');
531 my $req = $ses->request(
532 'open-ils.vandelay.bib_record.list.import',
533 $e->authtoken, \@vqbr_ids, $vandelay);
535 # pull the responses, noting all that were successfully imported
537 while (my $resp = $req->recv(timeout => 600)) {
538 my $stat = $resp->content;
540 if(!$stat or $U->event_code($stat)) { # import failure
541 $logger->error("acq-vl: error importing vandelay record " . Dumper($stat));
545 # "imported" refers to the vqbr id, not the
546 # success/failure of the vqbr merge attempt
547 next unless $stat->{imported};
549 my ($imported) = grep {$_->queued_record eq $stat->{imported}} @lis;
550 my $li_id = $imported->id;
552 if ($stat->{no_import}) {
553 $logger->info("acq-vl: acq lineitem $li_id did not import");
555 } else { # successful import
557 push(@success_lis, $li_id);
560 $logger->info("acq-vl: acq lineitem $li_id successfully merged/imported");
565 $logger->info("acq-vl: successfully imported lineitems [@success_lis]");
567 # add the successfully imported lineitems to the already-imported lineitems
568 push (@{$res->{li_ids}}, @success_lis);
573 # returns event on error, undef on success
574 sub check_import_li_marc_perms {
575 my($mgr, $li_ids) = @_;
577 # if there are any order records that are not linked to
578 # in-db bib records, verify staff has perms to import order records
579 my $order_li = $mgr->editor->search_acq_lineitem(
580 [{id => $li_ids, eg_bib_id => undef}, {limit => 1}], {idlist => 1})->[0];
583 return $mgr->editor->die_event unless
584 $mgr->editor->allowed('IMPORT_ACQ_LINEITEM_BIB_RECORD');
591 # ----------------------------------------------------------------------------
592 # if all of the lineitem details for this lineitem have
593 # been received, mark the lineitem as received
594 # returns 1 on non-received, li on received, 0 on error
595 # ----------------------------------------------------------------------------
597 sub describe_affected_po {
600 my ($enc, $spent, $estimated) =
601 OpenILS::Application::Acq::Financials::build_price_summary(
606 "state" => $po->state,
607 "amount_encumbered" => $enc,
608 "amount_spent" => $spent,
609 "amount_estimated" => $estimated
614 sub check_lineitem_received {
615 my($mgr, $li_id) = @_;
617 my $non_recv = $mgr->editor->search_acq_lineitem_detail(
618 {recv_time => undef, lineitem => $li_id}, {idlist=>1});
620 return 1 if @$non_recv;
622 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
623 $li->state('received');
624 return update_lineitem($mgr, $li);
627 sub receive_lineitem {
628 my($mgr, $li_id, $skip_complete_check) = @_;
629 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
631 return 0 unless $li->state eq 'on-order' or $li->state eq 'cancelled'; # sic
633 $li->clear_cancel_reason; # un-cancel on receive
635 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
636 {lineitem => $li_id, recv_time => undef}, {idlist => 1});
638 for my $lid_id (@$lid_ids) {
639 receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
643 $li->state('received');
645 $li = update_lineitem($mgr, $li) or return 0;
646 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
650 $skip_complete_check or (
651 $po = check_purchase_order_received($mgr, $li->purchase_order)
654 my $result = {"li" => {$li->id => {"state" => $li->state}}};
655 $result->{"po"} = describe_affected_po($mgr->editor, $po) if ref $po;
659 sub rollback_receive_lineitem {
660 my($mgr, $li_id) = @_;
661 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
663 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
664 {lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
666 for my $lid_id (@$lid_ids) {
667 rollback_receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
671 $li->state('on-order');
672 return update_lineitem($mgr, $li);
676 sub create_lineitem_status_events {
677 my($mgr, $li_id, $hook) = @_;
679 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
681 my $user_reqs = $mgr->editor->search_acq_user_request([
682 {lineitem => $li_id},
683 {flesh => 1, flesh_fields => {aur => ['usr']}}
686 for my $user_req (@$user_reqs) {
687 my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $user_req->usr->home_ou);
695 # ----------------------------------------------------------------------------
697 # ----------------------------------------------------------------------------
698 sub create_lineitem_detail {
699 my($mgr, %args) = @_;
700 my $lid = Fieldmapper::acq::lineitem_detail->new;
701 $lid->$_($args{$_}) for keys %args;
704 return $mgr->editor->create_acq_lineitem_detail($lid);
708 # flesh out any required data with default values where appropriate
709 sub complete_lineitem_detail {
711 unless($lid->barcode) {
712 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
713 $lid->barcode($pfx.$lid->id);
716 unless($lid->cn_label) {
717 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
718 $lid->cn_label($pfx.$lid->id);
721 if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
722 $lid->location($loc);
725 $lid->circ_modifier(get_default_circ_modifier($mgr, $lid->owning_lib))
726 unless defined $lid->circ_modifier;
728 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
732 sub get_default_circ_modifier {
734 my $code = $mgr->cache($org, 'def_circ_mod');
735 $code = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier') unless defined $code;
736 return $mgr->cache($org, 'def_circ_mod', $code) if defined $code;
740 sub delete_lineitem_detail {
742 $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
743 return $mgr->editor->delete_acq_lineitem_detail($lid);
747 sub receive_lineitem_detail {
748 my($mgr, $lid_id, $skip_complete_check) = @_;
749 my $e = $mgr->editor;
751 my $lid = $e->retrieve_acq_lineitem_detail([
755 acqlid => ['fund_debit']
760 return 1 if $lid->recv_time;
762 # if the LID is marked as canceled, remove the cancel reason,
763 # and reinstate fund debits where deleted by cancelation.
764 if ($lid->cancel_reason) {
765 my $cr = $e->retrieve_acq_cancel_reason($lid->cancel_reason);
767 if (!$U->is_true($cr->keep_debits)) {
768 # debits were removed during cancelation.
769 create_lineitem_detail_debit(
770 $mgr, $lid->lineitem, $lid) or return 0;
772 $lid->clear_cancel_reason;
775 $lid->receiver($e->requestor->id);
776 $lid->recv_time('now');
777 $e->update_acq_lineitem_detail($lid) or return 0;
779 if ($lid->eg_copy_id) {
780 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
781 # only update status if it hasn't already been updated
782 if ($copy->status == OILS_COPY_STATUS_ON_ORDER) {
783 my $custom_status = $U->ou_ancestor_setting_value(
784 $e->requestor->ws_ou, 'acq.copy_status_on_receiving', $e);
785 my $new_status = $custom_status || OILS_COPY_STATUS_IN_PROCESS;
786 $copy->status($new_status);
788 $copy->edit_date('now');
789 $copy->editor($e->requestor->id);
790 $copy->creator($e->requestor->id) if $U->ou_ancestor_setting_value(
791 $e->requestor->ws_ou, 'acq.copy_creator_uses_receiver', $e);
792 $e->update_asset_copy($copy) or return 0;
797 return 1 if $skip_complete_check;
799 my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
800 return 1 if $li == 1; # li not received
802 return check_purchase_order_received($mgr, $li->purchase_order);
806 sub rollback_receive_lineitem_detail {
807 my($mgr, $lid_id) = @_;
808 my $e = $mgr->editor;
810 my $lid = $e->retrieve_acq_lineitem_detail([
814 acqlid => ['fund_debit']
819 return 1 unless $lid->recv_time;
821 $lid->clear_receiver;
822 $lid->clear_recv_time;
823 $e->update_acq_lineitem_detail($lid) or return 0;
825 if ($lid->eg_copy_id) {
826 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
827 $copy->status(OILS_COPY_STATUS_ON_ORDER);
828 $copy->edit_date('now');
829 $copy->editor($e->requestor->id);
830 $e->update_asset_copy($copy) or return 0;
837 # ----------------------------------------------------------------------------
839 # ----------------------------------------------------------------------------
840 sub set_lineitem_attr {
841 my($mgr, %args) = @_;
842 my $attr_type = $args{attr_type};
844 # first, see if it's already set. May just need to overwrite it
845 my $attr = $mgr->editor->search_acq_lineitem_attr({
846 lineitem => $args{lineitem},
847 attr_type => $args{attr_type},
848 attr_name => $args{attr_name}
852 $attr->attr_value($args{attr_value});
853 return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
858 $attr = Fieldmapper::acq::lineitem_attr->new;
859 $attr->$_($args{$_}) for keys %args;
861 unless($attr->definition) {
862 my $find = "search_acq_$attr_type";
863 my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
864 $attr->definition($attr_def_id);
866 return $mgr->editor->create_acq_lineitem_attr($attr);
870 # ----------------------------------------------------------------------------
872 # ----------------------------------------------------------------------------
873 sub create_lineitem_debits {
874 my ($mgr, $li, $options) = @_;
876 my $dry_run = $options->{dry_run};
878 unless($li->estimated_unit_price) {
879 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
880 $mgr->editor->rollback;
884 unless($li->provider) {
885 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
886 $mgr->editor->rollback;
890 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
891 {lineitem => $li->id},
895 if (@$lid_ids == 0 and !$options->{zero_copy_activate}) {
896 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_COPIES', payload => $li->id));
897 $mgr->editor->rollback;
901 for my $lid_id (@$lid_ids) {
903 my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
906 flesh_fields => {acqlid => ['fund']}
910 create_lineitem_detail_debit($mgr, $li, $lid, $dry_run) or return 0;
919 sub create_lineitem_detail_debit {
920 my ($mgr, $li, $lid, $dry_run, $no_translate) = @_;
922 # don't create the debit if one already exists
923 return $mgr->editor->retrieve_acq_fund_debit($lid->fund_debit) if $lid->fund_debit;
925 my $li_id = ref($li) ? $li->id : $li;
927 unless(ref $li and ref $li->provider) {
928 $li = $mgr->editor->retrieve_acq_lineitem([
931 flesh_fields => {jub => ['provider']},
937 $lid->fund($mgr->editor->retrieve_acq_fund($lid->fund)) unless(ref $lid->fund);
939 $lid = $mgr->editor->retrieve_acq_lineitem_detail([
942 flesh_fields => {acqlid => ['fund']}
947 unless ($lid->fund) {
949 new OpenILS::Event("ACQ_FUND_NOT_FOUND") # close enough
954 my $amount = $li->estimated_unit_price;
955 if($li->provider->currency_type ne $lid->fund->currency_type and !$no_translate) {
957 # At Fund debit creation time, translate into the currency of the fund
958 # TODO: org setting to disable automatic currency conversion at debit create time?
960 $amount = $mgr->editor->json_query({
962 'acq.exchange_ratio',
963 $li->provider->currency_type, # source currency
964 $lid->fund->currency_type, # destination currency
965 $li->estimated_unit_price # source amount
967 })->[0]->{'acq.exchange_ratio'};
970 my $debit = create_fund_debit(
973 fund => $lid->fund->id,
974 origin_amount => $li->estimated_unit_price,
975 origin_currency_type => $li->provider->currency_type,
979 $lid->fund_debit($debit->id);
980 $lid->fund($lid->fund->id);
981 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
986 __PACKAGE__->register_method(
987 "method" => "fund_exceeds_balance_percent_api",
988 "api_name" => "open-ils.acq.fund.check_balance_percentages",
990 "desc" => q/Determine whether a given fund exceeds its defined
991 "balance stop and warning percentages"/,
993 {"desc" => "Authentication token", "type" => "string"},
994 {"desc" => "Fund ID", "type" => "number"},
995 {"desc" => "Theoretical debit amount (optional)",
998 "return" => {"desc" => q/An array of two values, for stop and warning,
999 in that order: 1 if fund exceeds that balance percentage, else 0/}
1003 sub fund_exceeds_balance_percent_api {
1004 my ($self, $conn, $auth, $fund_id, $debit_amount) = @_;
1006 $debit_amount ||= 0;
1008 my $e = new_editor("authtoken" => $auth);
1009 return $e->die_event unless $e->checkauth;
1011 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->die_event;
1012 return $e->die_event unless $e->allowed("VIEW_FUND", $fund->org);
1015 fund_exceeds_balance_percent($fund, $debit_amount, $e, "stop"),
1016 fund_exceeds_balance_percent($fund, $debit_amount, $e, "warning")
1023 sub fund_exceeds_balance_percent {
1024 my ($fund, $debit_amount, $e, $which) = @_;
1026 my ($method_name, $event_name) = @{{
1028 "balance_warning_percent", "ACQ_FUND_EXCEEDS_WARN_PERCENT"
1031 "balance_stop_percent", "ACQ_FUND_EXCEEDS_STOP_PERCENT"
1035 if ($fund->$method_name) {
1037 $e->search_acq_fund_combined_balance({"fund" => $fund->id})->[0];
1039 $e->search_acq_fund_allocation_total({"fund" => $fund->id})->[0];
1041 $balance = ($balance) ? $balance->amount : 0;
1042 $allocations = ($allocations) ? $allocations->amount : 0;
1045 $allocations == 0 || # if no allocations were ever made, assume we have hit the stop percent
1046 ((($allocations - $balance + $debit_amount) / $allocations) * 100) > $fund->$method_name
1048 $logger->info("fund would hit a limit: " . $fund->id . ", $balance, $debit_amount, $allocations, $method_name");
1053 "fund" => $fund, "debit_amount" => $debit_amount
1063 # ----------------------------------------------------------------------------
1065 # ----------------------------------------------------------------------------
1066 sub create_fund_debit {
1067 my($mgr, $dry_run, %args) = @_;
1069 # Verify the fund is not being spent beyond the hard stop amount
1070 my $fund = $mgr->editor->retrieve_acq_fund($args{fund}) or return 0;
1073 fund_exceeds_balance_percent(
1074 $fund, $args{"amount"}, $mgr->editor, "stop"
1077 $dry_run and fund_exceeds_balance_percent(
1078 $fund, $args{"amount"}, $mgr->editor, "warning"
1081 my $debit = Fieldmapper::acq::fund_debit->new;
1082 $debit->debit_type('purchase');
1083 $debit->encumbrance('t');
1084 $debit->$_($args{$_}) for keys %args;
1086 $mgr->add_debit($debit->amount);
1087 return $mgr->editor->create_acq_fund_debit($debit);
1091 # ----------------------------------------------------------------------------
1093 # ----------------------------------------------------------------------------
1094 sub create_picklist {
1095 my($mgr, %args) = @_;
1096 my $picklist = Fieldmapper::acq::picklist->new;
1097 $picklist->creator($mgr->editor->requestor->id);
1098 $picklist->owner($picklist->creator);
1099 $picklist->editor($picklist->creator);
1100 $picklist->create_time('now');
1101 $picklist->edit_time('now');
1102 $picklist->org_unit($mgr->editor->requestor->ws_ou);
1103 $picklist->owner($mgr->editor->requestor->id);
1104 $picklist->$_($args{$_}) for keys %args;
1105 $picklist->clear_id;
1106 $mgr->picklist($picklist);
1107 return $mgr->editor->create_acq_picklist($picklist);
1110 sub update_picklist {
1111 my($mgr, $picklist) = @_;
1112 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1113 $picklist->edit_time('now');
1114 $picklist->editor($mgr->editor->requestor->id);
1115 if ($mgr->editor->update_acq_picklist($picklist)) {
1116 $picklist = $mgr->editor->retrieve_acq_picklist($mgr->editor->data);
1117 $mgr->picklist($picklist);
1124 sub delete_picklist {
1125 my($mgr, $picklist) = @_;
1126 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1128 # delete all 'new' lineitems
1129 my $li_ids = $mgr->editor->search_acq_lineitem(
1131 picklist => $picklist->id,
1132 "-or" => {state => "new", purchase_order => undef}
1136 for my $li_id (@$li_ids) {
1137 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1138 return 0 unless delete_lineitem($mgr, $li);
1142 # detach all non-'new' lineitems
1143 $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}}, {idlist => 1});
1144 for my $li_id (@$li_ids) {
1145 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1146 $li->clear_picklist;
1147 return 0 unless update_lineitem($mgr, $li);
1151 # remove any picklist-specific object perms
1152 my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
1153 for my $op (@$ops) {
1154 return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
1157 return $mgr->editor->delete_acq_picklist($picklist);
1160 # ----------------------------------------------------------------------------
1162 # ----------------------------------------------------------------------------
1163 sub update_purchase_order {
1165 $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
1166 $po->editor($mgr->editor->requestor->id);
1167 $po->edit_time('now');
1168 $mgr->purchase_order($po);
1169 return $mgr->editor->retrieve_acq_purchase_order($mgr->editor->data)
1170 if $mgr->editor->update_acq_purchase_order($po);
1174 sub create_purchase_order {
1175 my($mgr, %args) = @_;
1177 # verify the chosen provider is still active
1178 my $provider = $mgr->editor->retrieve_acq_provider($args{provider}) or return 0;
1179 unless($U->is_true($provider->active)) {
1180 $logger->error("provider is not active. cannot create PO");
1181 $mgr->editor->event(OpenILS::Event->new('ACQ_PROVIDER_INACTIVE'));
1185 my $po = Fieldmapper::acq::purchase_order->new;
1186 $po->creator($mgr->editor->requestor->id);
1187 $po->editor($mgr->editor->requestor->id);
1188 $po->owner($mgr->editor->requestor->id);
1189 $po->edit_time('now');
1190 $po->create_time('now');
1191 $po->state('pending');
1192 $po->ordering_agency($mgr->editor->requestor->ws_ou);
1193 $po->$_($args{$_}) for keys %args;
1195 $mgr->purchase_order($po);
1196 return $mgr->editor->create_acq_purchase_order($po);
1199 # ----------------------------------------------------------------------------
1200 # if all of the lineitems for this PO are received and no
1201 # blanket charges are still encumbered, mark the PO as received.
1202 # ----------------------------------------------------------------------------
1203 sub check_purchase_order_received {
1204 my($mgr, $po_id) = @_;
1206 my $non_recv_li = $mgr->editor->json_query({
1211 "jub" => {"acqcr" => {"type" => "left"}}
1214 "+jub" => {"purchase_order" => $po_id},
1215 # Return lineitems that are not in the received/cancelled [sic]
1216 # state OR those that are canceled with keep_debits=true.
1219 "state" => {"not in" => ["received", "cancelled"]}}
1222 {"+jub" => {"state" => "cancelled"}},
1223 {"+acqcr" => {"keep_debits" =>"t"}}
1230 my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
1231 return $po if @$non_recv_li;
1233 # avoid marking the PO as received if any blanket charges
1234 # are still encumbered.
1235 my $blankets = $mgr->editor->json_query({
1236 select => {acqpoi => ['id']},
1239 aiit => {filter => {blanket=>'t'}},
1240 acqfdeb => {filter => {encumbrance => 't'}}
1243 where => {'+acqpoi' => {purchase_order => $po_id}}
1246 return $po if @$blankets;
1248 $po->state('received');
1249 return update_purchase_order($mgr, $po);
1253 # ----------------------------------------------------------------------------
1254 # Bib, Callnumber, and Copy data
1255 # ----------------------------------------------------------------------------
1257 sub create_lineitem_assets {
1258 my($mgr, $li_id) = @_;
1261 my $li = $mgr->editor->retrieve_acq_lineitem([
1264 flesh_fields => {jub => ['purchase_order', 'attributes']}
1268 # note: at this point, the bib record this LI links to should already be created
1270 # -----------------------------------------------------------------
1271 # The lineitem is going live, promote user request holds to real holds
1272 # -----------------------------------------------------------------
1273 promote_lineitem_holds($mgr, $li) or return 0;
1275 my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
1277 # -----------------------------------------------------------------
1278 # for each lineitem_detail, create the volume if necessary, create
1279 # a copy, and link them all together.
1280 # -----------------------------------------------------------------
1282 for my $lid_id (@{$li_details}) {
1284 my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
1285 next if $lid->eg_copy_id;
1287 # use the same callnumber label for all items within this lineitem
1288 $lid->cn_label($first_cn) if $first_cn and not $lid->cn_label;
1290 # apply defaults if necessary
1291 return 0 unless complete_lineitem_detail($mgr, $lid);
1293 $first_cn = $lid->cn_label unless $first_cn;
1295 my $org = $lid->owning_lib;
1296 my $label = $lid->cn_label;
1297 my $bibid = $li->eg_bib_id;
1299 my $volume = $mgr->cache($org, "cn.$bibid.$label");
1301 $volume = create_volume($mgr, $li, $lid) or return 0;
1302 $mgr->cache($org, "cn.$bibid.$label", $volume);
1304 create_copy($mgr, $volume, $lid, $li) or return 0;
1307 return { li => $li };
1311 my($mgr, $li, $lid) = @_;
1313 my ($volume, $evt) =
1314 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
1322 $mgr->editor->event($evt);
1330 my($mgr, $volume, $lid, $li) = @_;
1331 my $copy = Fieldmapper::asset::copy->new;
1333 $copy->loan_duration(2);
1334 $copy->fine_level(2);
1335 $copy->status(($lid->recv_time) ? OILS_COPY_STATUS_IN_PROCESS : OILS_COPY_STATUS_ON_ORDER);
1336 $copy->barcode($lid->barcode);
1337 $copy->location($lid->location);
1338 $copy->call_number($volume->id);
1339 $copy->circ_lib($volume->owning_lib);
1340 $copy->circ_modifier($lid->circ_modifier);
1342 # AKA list price. We might need a $li->list_price field since
1343 # estimated price is not necessarily the same as list price
1344 $copy->price($li->estimated_unit_price);
1346 my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
1348 $mgr->editor->event($evt);
1353 $lid->eg_copy_id($copy->id);
1354 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
1362 # ----------------------------------------------------------------------------
1363 # Workflow: Build a selection list from a Z39.50 search
1364 # ----------------------------------------------------------------------------
1366 __PACKAGE__->register_method(
1367 method => 'zsearch',
1368 api_name => 'open-ils.acq.picklist.search.z3950',
1371 desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
1373 {desc => 'Authentication token', type => 'string'},
1374 {desc => 'Search definition', type => 'object'},
1375 {desc => 'Picklist name, optional', type => 'string'},
1381 my($self, $conn, $auth, $search, $name, $options) = @_;
1382 my $e = new_editor(authtoken=>$auth);
1383 return $e->event unless $e->checkauth;
1384 return $e->event unless $e->allowed('CREATE_PICKLIST');
1386 $search->{limit} ||= 10;
1389 my $ses = OpenSRF::AppSession->create('open-ils.search');
1390 my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
1395 while(my $resp = $req->recv(timeout=>60)) {
1398 my $e = new_editor(requestor=>$e->requestor, xact=>1);
1399 $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1400 $picklist = zsearch_build_pl($mgr, $name);
1404 my $result = $resp->content;
1405 my $count = $result->{count} || 0;
1406 $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
1408 for my $rec (@{$result->{records}}) {
1410 my $li = create_lineitem($mgr,
1411 picklist => $picklist->id,
1412 source_label => $result->{service},
1413 marc => $rec->{marcxml},
1414 eg_bib_id => $rec->{bibid}
1417 if($$options{respond_li}) {
1418 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
1419 if $$options{flesh_attrs};
1420 $li->clear_marc if $$options{clear_marc};
1421 $mgr->respond(lineitem => $li);
1428 $mgr->editor->commit;
1429 return $mgr->respond_complete;
1432 sub zsearch_build_pl {
1433 my($mgr, $name) = @_;
1436 my $picklist = $mgr->editor->search_acq_picklist({
1437 owner => $mgr->editor->requestor->id,
1441 if($name eq '' and $picklist) {
1442 return 0 unless delete_picklist($mgr, $picklist);
1446 return update_picklist($mgr, $picklist) if $picklist;
1447 return create_picklist($mgr, name => $name);
1451 # ----------------------------------------------------------------------------
1452 # Workflow: Build a selection list / PO by importing a batch of MARC records
1453 # ----------------------------------------------------------------------------
1455 __PACKAGE__->register_method(
1456 method => 'upload_records',
1457 api_name => 'open-ils.acq.process_upload_records',
1459 max_chunk_count => 1
1462 sub upload_records {
1463 my($self, $conn, $auth, $key, $args) = @_;
1466 my $e = new_editor(authtoken => $auth, xact => 1);
1467 return $e->die_event unless $e->checkauth;
1468 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1470 my $cache = OpenSRF::Utils::Cache->new;
1472 my $data = $cache->get_cache("vandelay_import_spool_$key");
1473 my $filename = $data->{path};
1474 my $provider = $args->{provider};
1475 my $picklist = $args->{picklist};
1476 my $create_po = $args->{create_po};
1477 my $activate_po = $args->{activate_po};
1478 my $vandelay = $args->{vandelay};
1479 my $ordering_agency = $args->{ordering_agency} || $e->requestor->ws_ou;
1480 my $fiscal_year = $args->{fiscal_year};
1482 # if the user provides no fiscal year, find the
1483 # current fiscal year for the ordering agency.
1484 $fiscal_year ||= $U->simplereq(
1486 'open-ils.acq.org_unit.current_fiscal_year',
1494 unless(-r $filename) {
1495 $logger->error("unable to read MARC file $filename");
1497 return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
1500 $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
1503 $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
1504 if($picklist->owner != $e->requestor->id) {
1505 return $e->die_event unless
1506 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
1508 $mgr->picklist($picklist);
1512 return $e->die_event unless
1513 $e->allowed('CREATE_PURCHASE_ORDER', $ordering_agency);
1515 $po = create_purchase_order($mgr,
1516 ordering_agency => $ordering_agency,
1517 provider => $provider->id,
1518 state => 'pending' # will be updated later if activated
1519 ) or return $mgr->editor->die_event;
1522 $logger->info("acq processing MARC file=$filename");
1524 my $batch = new MARC::Batch ('USMARC', $filename);
1532 my ($err, $xml, $r);
1537 } catch Error with {
1539 $logger->warn("Proccessing of record $count in set $key failed with error $err. Skipping this record");
1546 $xml = clean_marc($r);
1547 } catch Error with {
1549 $logger->warn("Proccessing XML of record $count in set $key failed with error $err. Skipping this record");
1552 next if $err or not $xml;
1555 source_label => $provider->code,
1556 provider => $provider->id,
1560 $args{picklist} = $picklist->id if $picklist;
1562 $args{purchase_order} = $po->id;
1563 $args{state} = 'pending-order';
1566 my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
1568 $li->provider($provider); # flesh it, we'll need it later
1570 import_lineitem_details($mgr, $ordering_agency, $li, $fiscal_year)
1571 or return $mgr->editor->die_event;
1574 push(@li_list, $li->id);
1579 $evt = extract_po_name($mgr, $po, \@li_list);
1580 return $evt if $evt;
1585 $cache->delete_cache('vandelay_import_spool_' . $key);
1587 if ($po and $activate_po) {
1588 my $die_event = activate_purchase_order_impl($mgr, $po->id, $vandelay);
1589 return $die_event if $die_event;
1591 } elsif ($vandelay) {
1592 $vandelay->{new_rec_perm} = 'IMPORT_ACQ_LINEITEM_BIB_RECORD_UPLOAD';
1593 create_lineitem_list_assets($mgr, \@li_list, $vandelay,
1594 !$vandelay->{create_assets}) or return $e->die_event;
1597 return $mgr->respond_complete;
1600 # see if the PO name is encoded in the newly imported records
1601 sub extract_po_name {
1602 my ($mgr, $po, $li_ids) = @_;
1603 my $e = $mgr->editor;
1605 # find the first instance of the name
1606 my $attr = $e->search_acq_lineitem_attr([
1607 { lineitem => $li_ids,
1608 attr_type => 'lineitem_provider_attr_definition',
1609 attr_name => 'purchase_order'
1611 order_by => {aqlia => 'id'},
1614 ])->[0] or return undef;
1616 my $name = $attr->attr_value;
1618 # see if another PO already has the name, provider, and org
1619 my $existing = $e->search_acq_purchase_order(
1621 ordering_agency => $po->ordering_agency,
1622 provider => $po->provider
1627 # if a PO exists with the same name (and provider/org)
1628 # tack the po ID into the name to differentiate
1629 $name = sprintf("$name (%s)", $po->id) if $existing;
1631 $logger->info("Extracted PO name: $name");
1634 update_purchase_order($mgr, $po) or return $e->die_event;
1638 sub import_lineitem_details {
1639 my($mgr, $ordering_agency, $li, $fiscal_year) = @_;
1641 my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
1642 return 1 unless @$holdings;
1643 my $org_path = $U->get_org_ancestors($ordering_agency);
1644 $org_path = [ reverse (@$org_path) ];
1650 # create a lineitem detail for each copy in the data
1652 my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx, $fiscal_year);
1653 last unless defined $compiled;
1654 return 0 unless $compiled;
1656 # this takes the price of the last copy and uses it as the lineitem price
1657 # need to determine if a given record would include different prices for the same item
1658 $price = $$compiled{estimated_price};
1660 last unless $$compiled{quantity};
1662 for(1..$$compiled{quantity}) {
1663 my $lid = create_lineitem_detail(
1665 lineitem => $li->id,
1666 owning_lib => $$compiled{owning_lib},
1667 cn_label => $$compiled{call_number},
1668 fund => $$compiled{fund},
1669 circ_modifier => $$compiled{circ_modifier},
1670 note => $$compiled{note},
1671 location => $$compiled{copy_location},
1672 collection_code => $$compiled{collection_code},
1673 barcode => $$compiled{barcode}
1681 $li->estimated_unit_price($price);
1682 update_lineitem($mgr, $li) or return 0;
1686 # return hash on success, 0 on error, undef on no more holdings
1687 sub extract_lineitem_detail_data {
1688 my($mgr, $org_path, $holdings, $index, $fiscal_year) = @_;
1690 my @data_list = grep { $_->{holding} eq $index } @$holdings;
1691 return undef unless @data_list;
1693 my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1694 my $base_org = $$org_path[0];
1698 $logger->error("Item import extraction error: $msg");
1699 $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1700 $mgr->editor->rollback;
1701 $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1705 # ---------------------------------------------------------------------
1707 if(my $code = $compiled{fund_code}) {
1709 my $fund = $mgr->cache($base_org, "fund.$code");
1711 # search up the org tree for the most appropriate fund
1712 for my $org (@$org_path) {
1713 $fund = $mgr->editor->search_acq_fund(
1714 {org => $org, code => $code, year => $fiscal_year}, {idlist => 1})->[0];
1718 return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1719 $compiled{fund} = $fund;
1720 $mgr->cache($base_org, "fund.$code", $fund);
1724 # ---------------------------------------------------------------------
1726 if(my $sn = $compiled{owning_lib}) {
1727 my $org_id = $mgr->cache($base_org, "orgsn.$sn") ||
1728 $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1729 return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1730 $compiled{owning_lib} = $org_id;
1731 $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1735 # ---------------------------------------------------------------------
1737 my $code = $compiled{circ_modifier};
1741 # verify this is a valid circ modifier
1742 return $killme->("invlalid circ_modifier $code") unless
1743 defined $mgr->cache($base_org, "mod.$code") or
1744 $mgr->editor->retrieve_config_circ_modifier($code);
1746 # if valid, cache for future tests
1747 $mgr->cache($base_org, "mod.$code", $code);
1750 $compiled{circ_modifier} = get_default_circ_modifier($mgr, $base_org);
1754 # ---------------------------------------------------------------------
1756 if( my $name = $compiled{copy_location}) {
1758 my $cp_base_org = $base_org;
1760 if ($compiled{owning_lib}) {
1761 # start looking for copy locations at the copy
1762 # owning lib instaed of the upload context org
1763 $cp_base_org = $compiled{owning_lib};
1766 my $loc = $mgr->cache($cp_base_org, "copy_loc.$name");
1768 my $org = $cp_base_org;
1770 $loc = $mgr->editor->search_asset_copy_location(
1771 {owning_lib => $org, name => $name, deleted => 'f'}, {idlist => 1})->[0];
1773 $org = $mgr->editor->retrieve_actor_org_unit($org)->parent_ou;
1776 return $killme->("Invalid copy location $name") unless $loc;
1777 $compiled{copy_location} = $loc;
1778 $mgr->cache($cp_base_org, "copy_loc.$name", $loc);
1786 # ----------------------------------------------------------------------------
1787 # Workflow: Given an existing purchase order, import/create the bibs,
1788 # callnumber and copy objects
1789 # ----------------------------------------------------------------------------
1791 __PACKAGE__->register_method(
1792 method => 'create_po_assets',
1793 api_name => 'open-ils.acq.purchase_order.assets.create',
1795 desc => q/Creates assets for each lineitem in the purchase order/,
1797 {desc => 'Authentication token', type => 'string'},
1798 {desc => 'The purchase order id', type => 'number'},
1800 return => {desc => 'Streams a total versus completed counts object, event on error'}
1802 max_chunk_count => 1
1805 sub create_po_assets {
1806 my($self, $conn, $auth, $po_id, $args) = @_;
1809 my $e = new_editor(authtoken=>$auth, xact=>1);
1810 return $e->die_event unless $e->checkauth;
1811 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1813 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1815 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1817 # it's ugly, but it's fast. Get the total count of lineitem detail objects to process
1818 my $lid_total = $e->json_query({
1819 select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] },
1825 join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1829 where => {'+acqpo' => {id => $po_id}}
1832 # maximum number of Vandelay bib actions is twice
1833 # the number line items (queue bib, then create it)
1834 $mgr->total(scalar(@$li_ids) * 2 + $lid_total);
1836 create_lineitem_list_assets($mgr, $li_ids, $args->{vandelay})
1837 or return $e->die_event;
1840 update_purchase_order($mgr, $po) or return $e->die_event;
1843 return $mgr->respond_complete;
1848 __PACKAGE__->register_method(
1849 method => 'create_purchase_order_api',
1850 api_name => 'open-ils.acq.purchase_order.create',
1852 desc => 'Creates a new purchase order',
1854 {desc => 'Authentication token', type => 'string'},
1855 {desc => 'purchase_order to create', type => 'object'}
1857 return => {desc => 'The purchase order id, Event on failure'}
1859 max_chunk_count => 1
1862 sub create_purchase_order_api {
1863 my($self, $conn, $auth, $po, $args) = @_;
1866 my $e = new_editor(xact=>1, authtoken=>$auth);
1867 return $e->die_event unless $e->checkauth;
1868 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1869 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1872 my %pargs = (ordering_agency => $e->requestor->ws_ou); # default
1873 $pargs{provider} = $po->provider if $po->provider;
1874 $pargs{ordering_agency} = $po->ordering_agency if $po->ordering_agency;
1875 $pargs{prepayment_required} = $po->prepayment_required if $po->prepayment_required;
1876 $pargs{name} = $po->name if $po->name;
1877 my $vandelay = $args->{vandelay};
1879 $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1881 my $li_ids = $$args{lineitems};
1885 for my $li_id (@$li_ids) {
1887 my $li = $e->retrieve_acq_lineitem([
1889 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1890 ]) or return $e->die_event;
1892 return $e->die_event(
1894 "BAD_PARAMS", payload => $li,
1895 note => "acq.lineitem #" . $li->id .
1896 ": purchase_order #" . $li->purchase_order
1898 ) if $li->purchase_order;
1900 $li->provider($po->provider);
1901 $li->purchase_order($po->id);
1902 $li->state('pending-order');
1903 update_lineitem($mgr, $li) or return $e->die_event;
1908 # see if we have a PO name encoded in any of our lineitems
1909 my $evt = extract_po_name($mgr, $po, $li_ids);
1910 return $evt if $evt;
1912 # commit before starting the asset creation
1918 create_lineitem_list_assets(
1919 $mgr, $li_ids, $vandelay, !$$args{create_assets})
1920 or return $e->die_event;
1924 apply_default_copies($mgr, $po) or return $e->die_event;
1928 return $mgr->respond_complete;
1931 # !transaction must be managed by the caller
1932 # creates the default number of copies for each lineitem on the PO.
1933 # when a LI already has copies attached, no default copies are added.
1934 # without li_id, all lineitems are checked/applied
1935 # returns 1 on success, 0 on error
1936 sub apply_default_copies {
1937 my ($mgr, $po, $li_id) = @_;
1939 my $e = $mgr->editor;
1941 my $provider = ref($po->provider) ? $po->provider :
1942 $e->retrieve_acq_provider($po->provider);
1944 my $copy_count = $provider->default_copy_count || return 1;
1946 $logger->info("Applying $copy_count default copies for PO ".$po->id);
1948 my $li_ids = $li_id ? [$li_id] :
1949 $e->search_acq_lineitem({
1950 purchase_order => $po->id,
1951 cancel_reason => undef
1956 for my $li_id (@$li_ids) {
1958 my $lid_ids = $e->search_acq_lineitem_detail(
1959 {lineitem => $li_id}, {idlist => 1});
1961 # do not apply default copies when copies already exist
1964 for (1 .. $copy_count) {
1965 create_lineitem_detail($mgr,
1967 owning_lib => $e->requestor->ws_ou
1977 __PACKAGE__->register_method(
1978 method => 'update_lineitem_fund_batch',
1979 api_name => 'open-ils.acq.lineitem.fund.update.batch',
1982 desc => q/Given a set of lineitem IDS, updates the fund for all attached lineitem details/
1986 sub update_lineitem_fund_batch {
1987 my($self, $conn, $auth, $li_ids, $fund_id) = @_;
1988 my $e = new_editor(xact=>1, authtoken=>$auth);
1989 return $e->die_event unless $e->checkauth;
1990 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1991 for my $li_id (@$li_ids) {
1992 my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
1993 return $evt if $evt;
1994 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id});
1995 $_->fund($fund_id) and $_->ischanged(1) for @$li_details;
1996 $evt = lineitem_detail_CUD_batch($mgr, $li_details);
1997 return $evt if $evt;
2002 return $mgr->respond_complete;
2007 __PACKAGE__->register_method(
2008 method => 'lineitem_detail_CUD_batch_api',
2009 api_name => 'open-ils.acq.lineitem_detail.cud.batch',
2012 desc => q/Creates a new purchase order line item detail. / .
2013 q/Additionally creates the associated fund_debit/,
2015 {desc => 'Authentication token', type => 'string'},
2016 {desc => 'List of lineitem_details to create', type => 'array'},
2017 {desc => 'Create Debits. Used for creating post-po-asset-creation debits', type => 'bool'},
2019 return => {desc => 'Streaming response of current position in the array'}
2023 __PACKAGE__->register_method(
2024 method => 'lineitem_detail_CUD_batch_api',
2025 api_name => 'open-ils.acq.lineitem_detail.cud.batch.dry_run',
2029 Dry run version of open-ils.acq.lineitem_detail.cud.batch.
2030 In dry_run mode, updated fund_debit's the exceed the warning
2031 percent return an event.
2037 sub lineitem_detail_CUD_batch_api {
2038 my($self, $conn, $auth, $li_details, $create_debits) = @_;
2039 my $e = new_editor(xact=>1, authtoken=>$auth);
2040 return $e->die_event unless $e->checkauth;
2041 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2042 my $dry_run = ($self->api_name =~ /dry_run/o);
2043 my $evt = lineitem_detail_CUD_batch($mgr, $li_details, $create_debits, $dry_run);
2044 return $evt if $evt;
2046 return $mgr->respond_complete;
2050 sub lineitem_detail_CUD_batch {
2051 my($mgr, $li_details, $create_debits, $dry_run) = @_;
2053 $mgr->total(scalar(@$li_details));
2054 my $e = $mgr->editor;
2058 my $fund_cache = {};
2061 for my $lid (@$li_details) {
2063 unless($li = $li_cache{$lid->lineitem}) {
2064 ($li, $evt) = fetch_and_check_li($e, $lid->lineitem, 'write');
2065 return $evt if $evt;
2069 $lid = create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
2070 if($create_debits) {
2071 $li->provider($e->retrieve_acq_provider($li->provider)) or return $e->die_event;
2072 $lid->fund($e->retrieve_acq_fund($lid->fund)) or return $e->die_event;
2073 create_lineitem_detail_debit($mgr, $li, $lid, 0, 1) or return $e->die_event;
2076 } elsif($lid->ischanged) {
2077 return $evt if $evt = handle_changed_lid($e, $lid, $dry_run, $fund_cache);
2079 } elsif($lid->isdeleted) {
2080 delete_lineitem_detail($mgr, $lid) or return $e->die_event;
2083 $mgr->respond(li => $li);
2084 $li_cache{$lid->lineitem} = $li;
2090 sub handle_changed_lid {
2091 my($e, $lid, $dry_run, $fund_cache) = @_;
2093 my $orig_lid = $e->retrieve_acq_lineitem_detail($lid->id) or return $e->die_event;
2095 # updating the fund, so update the debit
2096 if($orig_lid->fund_debit and $orig_lid->fund != $lid->fund) {
2098 my $debit = $e->retrieve_acq_fund_debit($orig_lid->fund_debit);
2099 my $new_fund = $$fund_cache{$lid->fund} =
2100 $$fund_cache{$lid->fund} || $e->retrieve_acq_fund($lid->fund);
2102 # check the thresholds
2103 return $e->die_event if
2104 fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "stop");
2105 return $e->die_event if $dry_run and
2106 fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "warning");
2108 $debit->fund($new_fund->id);
2109 $e->update_acq_fund_debit($debit) or return $e->die_event;
2112 $e->update_acq_lineitem_detail($lid) or return $e->die_event;
2117 __PACKAGE__->register_method(
2118 method => 'receive_po_api',
2119 api_name => 'open-ils.acq.purchase_order.receive'
2122 sub receive_po_api {
2123 my($self, $conn, $auth, $po_id) = @_;
2124 my $e = new_editor(xact => 1, authtoken => $auth);
2125 return $e->die_event unless $e->checkauth;
2126 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2128 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2129 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2131 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2133 for my $li_id (@$li_ids) {
2134 receive_lineitem($mgr, $li_id) or return $e->die_event;
2138 $po->state('received');
2139 update_purchase_order($mgr, $po) or return $e->die_event;
2142 return $mgr->respond_complete;
2146 # At the moment there's a lack of parallelism between the receive and unreceive
2147 # API methods for POs and the API methods for LIs and LIDs. The methods for
2148 # POs stream back objects as they act, whereas the methods for LIs and LIDs
2149 # atomically return an object that describes only what changed (in LIs and LIDs
2150 # themselves or in the objects to which to LIs and LIDs belong).
2152 # The methods for LIs and LIDs work the way they do to faciliate the UI's
2153 # maintaining correct information about the state of these things when a user
2154 # wants to receive or unreceive these objects without refreshing their whole
2155 # display. The UI feature for receiving and un-receiving a whole PO just
2156 # refreshes the whole display, so this absence of parallelism in the UI is also
2157 # relected in this module.
2159 # This could be neatened in the future by making POs receive and unreceive in
2160 # the same way the LIs and LIDs do.
2162 __PACKAGE__->register_method(
2163 method => 'receive_lineitem_detail_api',
2164 api_name => 'open-ils.acq.lineitem_detail.receive',
2166 desc => 'Mark a lineitem_detail as received',
2168 {desc => 'Authentication token', type => 'string'},
2169 {desc => 'lineitem detail ID', type => 'number'}
2172 "on success, object describing changes to LID and possibly " .
2173 "to LI and PO; on error, Event"
2178 sub receive_lineitem_detail_api {
2179 my($self, $conn, $auth, $lid_id) = @_;
2181 my $e = new_editor(xact=>1, authtoken=>$auth);
2182 return $e->die_event unless $e->checkauth;
2183 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2186 "flesh" => 2, "flesh_fields" => {
2187 "acqlid" => ["lineitem"], "jub" => ["purchase_order"]
2191 my $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2193 return $e->die_event unless $e->allowed(
2194 'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
2197 my $recvd = receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
2199 # .. and re-retrieve
2200 $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2202 # Now build result data structure.
2203 my $result = {"lid" => {$lid->id => {"recv_time" => $lid->recv_time}}};
2206 if ($recvd->class_name =~ /::purchase_order/) {
2207 $result->{"po"} = describe_affected_po($e, $recvd);
2209 $lid->lineitem->id => {"state" => $lid->lineitem->state}
2211 } elsif ($recvd->class_name =~ /::lineitem/) {
2212 $result->{"li"} = {$recvd->id => {"state" => $recvd->state}};
2216 describe_affected_po($e, $lid->lineitem->purchase_order);
2222 __PACKAGE__->register_method(
2223 method => 'receive_lineitem_api',
2224 api_name => 'open-ils.acq.lineitem.receive',
2226 desc => 'Mark a lineitem as received',
2228 {desc => 'Authentication token', type => 'string'},
2229 {desc => 'lineitem ID', type => 'number'}
2232 "on success, object describing changes to LI and possibly PO; " .
2238 sub receive_lineitem_api {
2239 my($self, $conn, $auth, $li_id) = @_;
2241 my $e = new_editor(xact=>1, authtoken=>$auth);
2242 return $e->die_event unless $e->checkauth;
2243 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2245 my $li = $e->retrieve_acq_lineitem([
2249 jub => ['purchase_order']
2252 ]) or return $e->die_event;
2254 return $e->die_event unless $e->allowed(
2255 'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
2257 my $res = receive_lineitem($mgr, $li_id) or return $e->die_event;
2259 $conn->respond_complete($res);
2260 $mgr->run_post_response_hooks
2264 __PACKAGE__->register_method(
2265 method => 'receive_lineitem_batch_api',
2266 api_name => 'open-ils.acq.lineitem.receive.batch',
2269 desc => 'Mark lineitems as received',
2271 {desc => 'Authentication token', type => 'string'},
2272 {desc => 'lineitem ID list', type => 'array'}
2275 q/On success, stream of objects describing changes to LIs and
2276 possibly PO; onerror, Event. Any event, even after lots of other
2277 objects, should mean general failure of whole batch operation./
2282 sub receive_lineitem_batch_api {
2283 my ($self, $conn, $auth, $li_idlist) = @_;
2285 return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2287 my $e = new_editor(xact => 1, authtoken => $auth);
2288 return $e->die_event unless $e->checkauth;
2290 my $mgr = new OpenILS::Application::Acq::BatchManager(
2291 editor => $e, conn => $conn
2294 for my $li_id (map { int $_ } @$li_idlist) {
2295 my $li = $e->retrieve_acq_lineitem([
2298 flesh_fields => { jub => ['purchase_order'] }
2300 ]) or return $e->die_event;
2302 return $e->die_event unless $e->allowed(
2303 'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency
2306 receive_lineitem($mgr, $li_id) or return $e->die_event;
2310 $e->commit or return $e->die_event;
2311 $mgr->respond_complete;
2312 $mgr->run_post_response_hooks;
2315 __PACKAGE__->register_method(
2316 method => 'rollback_receive_po_api',
2317 api_name => 'open-ils.acq.purchase_order.receive.rollback'
2320 sub rollback_receive_po_api {
2321 my($self, $conn, $auth, $po_id) = @_;
2322 my $e = new_editor(xact => 1, authtoken => $auth);
2323 return $e->die_event unless $e->checkauth;
2324 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2326 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2327 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2329 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2331 for my $li_id (@$li_ids) {
2332 rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2336 $po->state('on-order');
2337 update_purchase_order($mgr, $po) or return $e->die_event;
2340 return $mgr->respond_complete;
2344 __PACKAGE__->register_method(
2345 method => 'rollback_receive_lineitem_detail_api',
2346 api_name => 'open-ils.acq.lineitem_detail.receive.rollback',
2348 desc => 'Mark a lineitem_detail as Un-received',
2350 {desc => 'Authentication token', type => 'string'},
2351 {desc => 'lineitem detail ID', type => 'number'}
2354 "on success, object describing changes to LID and possibly " .
2355 "to LI and PO; on error, Event"
2360 sub rollback_receive_lineitem_detail_api {
2361 my($self, $conn, $auth, $lid_id) = @_;
2363 my $e = new_editor(xact=>1, authtoken=>$auth);
2364 return $e->die_event unless $e->checkauth;
2365 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2367 my $lid = $e->retrieve_acq_lineitem_detail([
2371 acqlid => ['lineitem'],
2372 jub => ['purchase_order']
2376 my $li = $lid->lineitem;
2377 my $po = $li->purchase_order;
2379 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2383 my $recvd = rollback_receive_lineitem_detail($mgr, $lid_id)
2384 or return $e->die_event;
2387 $result->{"lid"} = {$recvd->id => {"recv_time" => $recvd->recv_time}};
2389 $result->{"lid"} = {$lid->id => {"recv_time" => $lid->recv_time}};
2392 if ($li->state eq "received") {
2393 $li->state("on-order");
2394 $li = update_lineitem($mgr, $li) or return $e->die_event;
2395 $result->{"li"} = {$li->id => {"state" => $li->state}};
2398 if ($po->state eq "received") {
2399 $po->state("on-order");
2400 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2402 $result->{"po"} = describe_affected_po($e, $po);
2404 $e->commit and return $result or return $e->die_event;
2407 __PACKAGE__->register_method(
2408 method => 'rollback_receive_lineitem_api',
2409 api_name => 'open-ils.acq.lineitem.receive.rollback',
2411 desc => 'Mark a lineitem as Un-received',
2413 {desc => 'Authentication token', type => 'string'},
2414 {desc => 'lineitem ID', type => 'number'}
2417 "on success, object describing changes to LI and possibly PO; " .
2423 sub rollback_receive_lineitem_api {
2424 my($self, $conn, $auth, $li_id) = @_;
2426 my $e = new_editor(xact=>1, authtoken=>$auth);
2427 return $e->die_event unless $e->checkauth;
2428 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2430 my $li = $e->retrieve_acq_lineitem([
2432 "flesh" => 1, "flesh_fields" => {"jub" => ["purchase_order"]}
2435 my $po = $li->purchase_order;
2437 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2439 $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2441 my $result = {"li" => {$li->id => {"state" => $li->state}}};
2442 if ($po->state eq "received") {
2443 $po->state("on-order");
2444 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2446 $result->{"po"} = describe_affected_po($e, $po);
2448 $e->commit and return $result or return $e->die_event;
2451 __PACKAGE__->register_method(
2452 method => 'rollback_receive_lineitem_batch_api',
2453 api_name => 'open-ils.acq.lineitem.receive.rollback.batch',
2456 desc => 'Mark a list of lineitems as Un-received',
2458 {desc => 'Authentication token', type => 'string'},
2459 {desc => 'lineitem ID list', type => 'array'}
2462 q/on success, a stream of objects describing changes to LI and
2463 possibly PO; on error, Event. Any event means all previously
2464 returned objects indicate changes that didn't really happen./
2469 sub rollback_receive_lineitem_batch_api {
2470 my ($self, $conn, $auth, $li_idlist) = @_;
2472 return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2474 my $e = new_editor(xact => 1, authtoken => $auth);
2475 return $e->die_event unless $e->checkauth;
2477 my $mgr = new OpenILS::Application::Acq::BatchManager(
2478 editor => $e, conn => $conn
2481 for my $li_id (map { int $_ } @$li_idlist) {
2482 my $li = $e->retrieve_acq_lineitem([
2485 "flesh_fields" => {"jub" => ["purchase_order"]}
2489 my $po = $li->purchase_order;
2491 return $e->die_event unless
2492 $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2494 $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2496 my $result = {"li" => {$li->id => {"state" => $li->state}}};
2497 if ($po->state eq "received") { # should happen first time, not after
2498 $po->state("on-order");
2499 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2501 $result->{"po"} = describe_affected_po($e, $po);
2503 $mgr->respond(%$result);
2506 $e->commit or return $e->die_event;
2507 $mgr->respond_complete;
2508 $mgr->run_post_response_hooks;
2512 __PACKAGE__->register_method(
2513 method => 'set_lineitem_price_api',
2514 api_name => 'open-ils.acq.lineitem.price.set',
2516 desc => 'Set lineitem price. If debits already exist, update them as well',
2518 {desc => 'Authentication token', type => 'string'},
2519 {desc => 'lineitem ID', type => 'number'}
2521 return => {desc => 'status blob, Event on error'}
2525 sub set_lineitem_price_api {
2526 my($self, $conn, $auth, $li_id, $price) = @_;
2528 my $e = new_editor(xact=>1, authtoken=>$auth);
2529 return $e->die_event unless $e->checkauth;
2530 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2532 my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
2533 return $evt if $evt;
2535 $li->estimated_unit_price($price);
2536 update_lineitem($mgr, $li) or return $e->die_event;
2538 my $lid_ids = $e->search_acq_lineitem_detail(
2539 {lineitem => $li_id, fund_debit => {'!=' => undef}},
2543 for my $lid_id (@$lid_ids) {
2545 my $lid = $e->retrieve_acq_lineitem_detail([
2547 flesh => 1, flesh_fields => {acqlid => ['fund', 'fund_debit']}}
2550 $lid->fund_debit->amount($price);
2551 $e->update_acq_fund_debit($lid->fund_debit) or return $e->die_event;
2557 return $mgr->respond_complete;
2561 __PACKAGE__->register_method(
2562 method => 'clone_picklist_api',
2563 api_name => 'open-ils.acq.picklist.clone',
2565 desc => 'Clones a picklist, including lineitem and lineitem details',
2567 {desc => 'Authentication token', type => 'string'},
2568 {desc => 'Picklist ID', type => 'number'},
2569 {desc => 'New Picklist Name', type => 'string'}
2571 return => {desc => 'status blob, Event on error'}
2575 sub clone_picklist_api {
2576 my($self, $conn, $auth, $pl_id, $name) = @_;
2578 my $e = new_editor(xact=>1, authtoken=>$auth);
2579 return $e->die_event unless $e->checkauth;
2580 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2582 my $old_pl = $e->retrieve_acq_picklist($pl_id);
2583 my $new_pl = create_picklist($mgr, %{$old_pl->to_bare_hash}, name => $name) or return $e->die_event;
2585 my $li_ids = $e->search_acq_lineitem({picklist => $pl_id}, {idlist => 1});
2587 # get the current user
2588 my $cloner = $mgr->editor->requestor->id;
2590 for my $li_id (@$li_ids) {
2592 # copy the lineitems' MARC
2593 my $marc = ($e->retrieve_acq_lineitem($li_id))->marc;
2595 # create a skeletal clone of the item
2596 my $li = Fieldmapper::acq::lineitem->new;
2597 $li->creator($cloner);
2598 $li->selector($cloner);
2599 $li->editor($cloner);
2602 my $new_li = create_lineitem($mgr, %{$li->to_bare_hash}, picklist => $new_pl->id) or return $e->die_event;
2608 return $mgr->respond_complete;
2612 __PACKAGE__->register_method(
2613 method => 'merge_picklist_api',
2614 api_name => 'open-ils.acq.picklist.merge',
2616 desc => 'Merges 2 or more picklists into a single list',
2618 {desc => 'Authentication token', type => 'string'},
2619 {desc => 'Lead Picklist ID', type => 'number'},
2620 {desc => 'List of subordinate picklist IDs', type => 'array'}
2622 return => {desc => 'status blob, Event on error'}
2626 sub merge_picklist_api {
2627 my($self, $conn, $auth, $lead_pl, $pl_list) = @_;
2629 my $e = new_editor(xact=>1, authtoken=>$auth);
2630 return $e->die_event unless $e->checkauth;
2631 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2633 # XXX perms on each picklist modified
2635 $lead_pl = $e->retrieve_acq_picklist($lead_pl) or return $e->die_event;
2636 # point all of the lineitems at the lead picklist
2637 my $li_ids = $e->search_acq_lineitem({picklist => $pl_list}, {idlist => 1});
2639 for my $li_id (@$li_ids) {
2640 my $li = $e->retrieve_acq_lineitem($li_id);
2641 $li->picklist($lead_pl);
2642 update_lineitem($mgr, $li) or return $e->die_event;
2646 # now delete the subordinate lists
2647 for my $pl_id (@$pl_list) {
2648 my $pl = $e->retrieve_acq_picklist($pl_id);
2649 $e->delete_acq_picklist($pl) or return $e->die_event;
2652 update_picklist($mgr, $lead_pl) or return $e->die_event;
2655 return $mgr->respond_complete;
2659 __PACKAGE__->register_method(
2660 method => 'delete_picklist_api',
2661 api_name => 'open-ils.acq.picklist.delete',
2663 desc => q/Deletes a picklist. It also deletes any lineitems in the "new" state. / .
2664 q/Other attached lineitems are detached/,
2666 {desc => 'Authentication token', type => 'string'},
2667 {desc => 'Picklist ID to delete', type => 'number'}
2669 return => {desc => '1 on success, Event on error'}
2673 sub delete_picklist_api {
2674 my($self, $conn, $auth, $picklist_id) = @_;
2675 my $e = new_editor(xact=>1, authtoken=>$auth);
2676 return $e->die_event unless $e->checkauth;
2677 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2678 my $pl = $e->retrieve_acq_picklist($picklist_id) or return $e->die_event;
2679 delete_picklist($mgr, $pl) or return $e->die_event;
2681 return $mgr->respond_complete;
2686 __PACKAGE__->register_method(
2687 method => 'activate_purchase_order',
2688 api_name => 'open-ils.acq.purchase_order.activate.dry_run'
2691 __PACKAGE__->register_method(
2692 method => 'activate_purchase_order',
2693 api_name => 'open-ils.acq.purchase_order.activate',
2695 desc => q/Activates a purchase order. This updates the status of the PO / .
2696 q/and Lineitems to 'on-order'. Activated PO's are ready for EDI delivery if appropriate./,
2698 {desc => 'Authentication token', type => 'string'},
2699 {desc => 'Purchase ID', type => 'number'}
2701 return => {desc => '1 on success, Event on error'}
2705 sub activate_purchase_order {
2706 my($self, $conn, $auth, $po_id, $vandelay, $options) = @_;
2708 $$options{dry_run} = ($self->api_name =~ /\.dry_run/) ? 1 : 0;
2710 my $e = new_editor(authtoken=>$auth);
2711 return $e->die_event unless $e->checkauth;
2712 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2713 my $die_event = activate_purchase_order_impl($mgr, $po_id, $vandelay, $options);
2714 return $e->die_event if $die_event;
2715 $conn->respond_complete(1);
2716 $mgr->run_post_response_hooks unless $$options{dry_run};
2720 # xacts managed within
2721 sub activate_purchase_order_impl {
2722 my ($mgr, $po_id, $vandelay, $options) = @_;
2724 my $dry_run = $$options{dry_run};
2725 my $no_assets = $$options{no_assets};
2727 # read-only until lineitem asset creation
2728 my $e = $mgr->editor;
2731 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2732 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
2734 return $e->die_event(OpenILS::Event->new('PO_ALREADY_ACTIVATED'))
2735 if $po->order_date; # PO cannot be re-activated
2737 my $provider = $e->retrieve_acq_provider($po->provider);
2739 # find lineitems and create assets for all
2742 purchase_order => $po_id,
2743 state => [qw/pending-order new order-ready/]
2746 my $li_ids = $e->search_acq_lineitem($query, {idlist => 1});
2748 my $vl_resp; # imported li's and the managing queue
2749 unless ($dry_run or $no_assets) {
2750 $e->rollback; # read-only thus far
2752 # list_assets manages its own transactions
2753 $vl_resp = create_lineitem_list_assets($mgr, $li_ids, $vandelay)
2754 or return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED');
2758 # create fund debits for lineitems
2760 for my $li_id (@$li_ids) {
2761 my $li = $e->retrieve_acq_lineitem($li_id);
2763 unless ($li->eg_bib_id or $dry_run or $no_assets) {
2764 # we encountered a lineitem that was not successfully imported.
2765 # we cannot continue. rollback and report.
2767 return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED', {queue => $vl_resp->{queue}});
2770 $li->state('on-order');
2771 $li->claim_policy($provider->default_claim_policy)
2772 if $provider->default_claim_policy and !$li->claim_policy;
2773 create_lineitem_debits($mgr, $li, $options) or return $e->die_event;
2774 update_lineitem($mgr, $li) or return $e->die_event;
2775 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li->id, 'aur.ordered'); });
2779 # create po-item debits
2781 for my $po_item (@{$e->search_acq_po_item({purchase_order => $po_id})}) {
2783 my $debit = create_fund_debit(
2786 debit_type => 'direct_charge', # to match invoicing
2787 origin_amount => $po_item->estimated_cost,
2788 origin_currency_type => $e->retrieve_acq_fund($po_item->fund)->currency_type,
2789 amount => $po_item->estimated_cost,
2790 fund => $po_item->fund
2791 ) or return $e->die_event;
2792 $po_item->fund_debit($debit->id);
2793 $e->update_acq_po_item($po_item) or return $e->die_event;
2797 # mark PO as ordered
2799 $po->state('on-order');
2800 $po->order_date('now');
2801 update_purchase_order($mgr, $po) or return $e->die_event;
2804 $dry_run and $e->rollback or $e->commit;
2806 # tell the world we activated a PO
2807 $U->create_events_for_hook('acqpo.activated', $po, $po->ordering_agency) unless $dry_run;
2813 __PACKAGE__->register_method(
2814 method => 'split_purchase_order_by_lineitems',
2815 api_name => 'open-ils.acq.purchase_order.split_by_lineitems',
2817 desc => q/Splits a PO into many POs, 1 per lineitem. Only works for / .
2818 q/POs a) with more than one lineitems, and b) in the "pending" state./,
2820 {desc => 'Authentication token', type => 'string'},
2821 {desc => 'Purchase order ID', type => 'number'}
2823 return => {desc => 'list of new PO IDs on success, Event on error'}
2827 sub split_purchase_order_by_lineitems {
2828 my ($self, $conn, $auth, $po_id) = @_;
2830 my $e = new_editor("xact" => 1, "authtoken" => $auth);
2831 return $e->die_event unless $e->checkauth;
2833 my $po = $e->retrieve_acq_purchase_order([
2836 "flesh_fields" => {"acqpo" => [qw/lineitems notes/]}
2838 ]) or return $e->die_event;
2840 return $e->die_event
2841 unless $e->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2843 unless ($po->state eq "pending") {
2845 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_LATE");
2848 unless (@{$po->lineitems} > 1) {
2850 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_SHORT");
2853 # To split an existing PO into many, it seems unwise to just delete the
2854 # original PO, so we'll instead detach all of the original POs' lineitems
2855 # but the first, then create new POs for each of the remaining LIs, and
2856 # then attach the LIs to their new POs.
2858 my @po_ids = ($po->id);
2859 my @moving_li = @{$po->lineitems};
2860 shift @moving_li; # discard first LI
2862 foreach my $li (@moving_li) {
2863 my $new_po = $po->clone;
2865 $new_po->clear_name;
2866 $new_po->creator($e->requestor->id);
2867 $new_po->editor($e->requestor->id);
2868 $new_po->owner($e->requestor->id);
2869 $new_po->edit_time("now");
2870 $new_po->create_time("now");
2872 $new_po = $e->create_acq_purchase_order($new_po);
2874 # Clone any notes attached to the old PO and attach to the new one.
2875 foreach my $note (@{$po->notes}) {
2876 my $new_note = $note->clone;
2877 $new_note->clear_id;
2878 $new_note->edit_time("now");
2879 $new_note->purchase_order($new_po->id);
2880 $e->create_acq_po_note($new_note);
2883 $li->edit_time("now");
2884 $li->purchase_order($new_po->id);
2885 $e->update_acq_lineitem($li);
2887 push @po_ids, $new_po->id;
2890 $po->edit_time("now");
2891 $e->update_acq_purchase_order($po);
2893 return \@po_ids if $e->commit;
2894 return $e->die_event;
2898 sub not_cancelable {
2900 (ref $o eq "HASH" and $o->{"textcode"} eq "ACQ_NOT_CANCELABLE");
2903 __PACKAGE__->register_method(
2904 method => "cancel_purchase_order_api",
2905 api_name => "open-ils.acq.purchase_order.cancel",
2907 desc => q/Cancels an on-order purchase order/,
2909 {desc => "Authentication token", type => "string"},
2910 {desc => "PO ID to cancel", type => "number"},
2911 {desc => "Cancel reason ID", type => "number"}
2913 return => {desc => q/Object describing changed POs, LIs and LIDs
2914 on success; Event on error./}
2918 sub cancel_purchase_order_api {
2919 my ($self, $conn, $auth, $po_id, $cancel_reason) = @_;
2921 my $e = new_editor("xact" => 1, "authtoken" => $auth);
2922 return $e->die_event unless $e->checkauth;
2923 my $mgr = new OpenILS::Application::Acq::BatchManager(
2924 "editor" => $e, "conn" => $conn
2927 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
2928 return new OpenILS::Event(
2929 "BAD_PARAMS", "note" => "Provide cancel reason ID"
2932 my $result = cancel_purchase_order($mgr, $po_id, $cancel_reason) or
2933 return $e->die_event;
2934 if (not_cancelable($result)) { # event not from CStoreEditor
2937 } elsif ($result == -1) {
2939 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
2942 $e->commit or return $e->die_event;
2944 # XXX create purchase order status events?
2946 if ($mgr->{post_commit}) {
2947 foreach my $func (@{$mgr->{post_commit}}) {
2955 sub cancel_purchase_order {
2956 my ($mgr, $po_id, $cancel_reason) = @_;
2958 my $po = $mgr->editor->retrieve_acq_purchase_order($po_id) or return 0;
2960 # XXX is "cancelled" a typo? It's not correct US spelling, anyway.
2961 # Depending on context, this may not warrant an event.
2962 return -1 if $po->state eq "cancelled";
2964 # But this always does.
2965 return new OpenILS::Event(
2966 "ACQ_NOT_CANCELABLE", "note" => "purchase_order $po_id"
2967 ) unless ($po->state eq "on-order" or $po->state eq "pending");
2970 $mgr->editor->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2972 $po->state("cancelled");
2973 $po->cancel_reason($cancel_reason->id);
2975 my $li_ids = $mgr->editor->search_acq_lineitem(
2976 {"purchase_order" => $po_id}, {"idlist" => 1}
2979 my $result = {"li" => {}, "lid" => {}};
2980 foreach my $li_id (@$li_ids) {
2981 my $li_result = cancel_lineitem($mgr, $li_id, $cancel_reason)
2984 next if $li_result == -1; # already canceled:skip.
2985 return $li_result if not_cancelable($li_result); # not cancelable:stop.
2987 # Merge in each LI result (there's only going to be
2988 # one per call to cancel_lineitem).
2989 my ($k, $v) = each %{$li_result->{"li"}};
2990 $result->{"li"}->{$k} = $v;
2992 # Merge in each LID result (there may be many per call to
2994 while (($k, $v) = each %{$li_result->{"lid"}}) {
2995 $result->{"lid"}->{$k} = $v;
2999 my $po_item_ids = $mgr->editor
3000 ->search_acq_po_item({purchase_order => $po_id}, {idlist => 1});
3002 for my $po_item_id (@$po_item_ids) {
3004 my $po_item = $mgr->editor->retrieve_acq_po_item([
3007 flesh_fields => {acqpoi => ['purchase_order', 'fund_debit']}
3009 ]) or return -1; # results in rollback
3011 # returns undef on success
3012 my $result = clear_po_item($mgr->editor, $po_item);
3014 return $result if not_cancelable($result);
3015 return -1 if $result; # other failure events, results in rollback
3019 # TODO who/what/where/how do we indicate this change for electronic orders?
3020 # TODO return changes to encumbered/spent
3021 # TODO maybe cascade up from smaller object to container object if last
3022 # smaller object in the container has been canceled?
3024 update_purchase_order($mgr, $po) or return 0;
3026 $po_id => {"state" => $po->state, "cancel_reason" => $cancel_reason}
3032 __PACKAGE__->register_method(
3033 method => "cancel_lineitem_api",
3034 api_name => "open-ils.acq.lineitem.cancel",
3036 desc => q/Cancels an on-order lineitem/,
3038 {desc => "Authentication token", type => "string"},
3039 {desc => "Lineitem ID to cancel", type => "number"},
3040 {desc => "Cancel reason ID", type => "number"}
3042 return => {desc => q/Object describing changed LIs and LIDs on success;
3047 __PACKAGE__->register_method(
3048 method => "cancel_lineitem_api",
3049 api_name => "open-ils.acq.lineitem.cancel.batch",
3051 desc => q/Batched version of open-ils.acq.lineitem.cancel/,
3052 return => {desc => q/Object describing changed LIs and LIDs on success;
3057 sub cancel_lineitem_api {
3058 my ($self, $conn, $auth, $li_id, $cancel_reason) = @_;
3060 my $batched = $self->api_name =~ /\.batch/;
3062 my $e = new_editor("xact" => 1, "authtoken" => $auth);
3063 return $e->die_event unless $e->checkauth;
3064 my $mgr = new OpenILS::Application::Acq::BatchManager(
3065 "editor" => $e, "conn" => $conn
3068 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
3069 return new OpenILS::Event(
3070 "BAD_PARAMS", "note" => "Provide cancel reason ID"
3073 my ($result, $maybe_event);
3076 $result = {"li" => {}, "lid" => {}};
3077 foreach my $one_li_id (@$li_id) {
3078 my $one = cancel_lineitem($mgr, $one_li_id, $cancel_reason) or
3079 return $e->die_event;
3080 if (not_cancelable($one)) {
3081 $maybe_event = $one;
3082 } elsif ($result == -1) {
3083 $maybe_event = new OpenILS::Event("ACQ_ALREADY_CANCELED");
3087 while (($k, $v) = each %{$one->{"li"}}) {
3088 $result->{"li"}->{$k} = $v;
3091 if ($one->{"lid"}) {
3092 while (($k, $v) = each %{$one->{"lid"}}) {
3093 $result->{"lid"}->{$k} = $v;
3099 $result = cancel_lineitem($mgr, $li_id, $cancel_reason) or
3100 return $e->die_event;
3102 if (not_cancelable($result)) {
3105 } elsif ($result == -1) {
3107 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3111 if ($batched and not scalar keys %{$result->{"li"}}) {
3113 return $maybe_event;
3115 $e->commit or return $e->die_event;
3116 # create_lineitem_status_events should handle array li_id ok
3117 create_lineitem_status_events($mgr, $li_id, "aur.cancelled");
3119 if ($mgr->{post_commit}) {
3120 foreach my $func (@{$mgr->{post_commit}}) {
3129 sub cancel_lineitem {
3130 my ($mgr, $li_id, $cancel_reason) = @_;
3132 my $li = $mgr->editor->retrieve_acq_lineitem([
3133 $li_id, {flesh => 1,
3134 flesh_fields => {jub => ['purchase_order','cancel_reason']}}
3137 return 0 unless $mgr->editor->allowed(
3138 "CREATE_PURCHASE_ORDER", $li->purchase_order->ordering_agency
3141 # Depending on context, this may not warrant an event.
3142 return -1 if $li->state eq "cancelled"
3143 and $li->cancel_reason->keep_debits eq 'f';
3145 # But this always does. Note that this used to be looser, but you can
3146 # no longer cancel lineitems that lack a PO or that are in "pending-order"
3147 # state (you could in the past).
3148 return new OpenILS::Event(
3149 "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id"
3150 ) unless $li->purchase_order and
3151 ($li->state eq "on-order" or $li->state eq "cancelled");
3153 $li->state("cancelled");
3154 $li->cancel_reason($cancel_reason->id);
3156 my $lids = $mgr->editor->search_acq_lineitem_detail([{
3157 "lineitem" => $li_id
3160 flesh_fields => { acqlid => ['eg_copy_id'] }
3163 my $result = {"lid" => {}};
3165 foreach my $lid (@$lids) {
3166 my $lid_result = cancel_lineitem_detail($mgr, $lid->id, $cancel_reason)
3169 # gathering any real copies for deletion
3170 if ($lid->eg_copy_id) {
3171 $lid->eg_copy_id->isdeleted('t');
3172 push @$copies, $lid->eg_copy_id;
3175 next if $lid_result == -1; # already canceled: just skip it.
3176 return $lid_result if not_cancelable($lid_result); # not cxlable: stop.
3178 # Merge in each LID result (there's only going to be one per call to
3179 # cancel_lineitem_detail).
3180 my ($k, $v) = each %{$lid_result->{"lid"}};
3181 $result->{"lid"}->{$k} = $v;
3184 # Attempt to delete the gathered copies (this will also handle volume deletion and bib deletion)
3185 # Delete empty bibs according org unit setting
3186 my $force_delete_empty_bib = $U->ou_ancestor_setting_value(
3187 $mgr->editor->requestor->ws_ou, 'cat.bib.delete_on_no_copy_via_acq_lineitem_cancel', $mgr->editor);
3188 if (scalar(@$copies)>0) {
3190 my $delete_stats = undef;
3191 my $retarget_holds = [];
3192 my $cat_evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
3193 $mgr->editor, $override, undef, $copies, $delete_stats, $retarget_holds,$force_delete_empty_bib);
3196 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($cat_evt));
3197 return new OpenILS::Event(
3198 "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id", "payload" => $cat_evt
3202 # We can't do the following and stay within the same transaction, but that's okay, the hold targeter will pick these up later.
3203 #my $ses = OpenSRF::AppSession->create('open-ils.circ');
3204 #$ses->request('open-ils.circ.hold.reset.batch', $auth, $retarget_holds);
3207 # if we have a bib, check to see whether it has been deleted. if so, cancel any active holds targeting that bib
3208 if ($li->eg_bib_id) {
3209 my $bib = $mgr->editor->retrieve_biblio_record_entry($li->eg_bib_id) or return new OpenILS::Event(
3210 "ACQ_NOT_CANCELABLE", "note" => "Could not retrieve bib " . $li->eg_bib_id . " for lineitem $li_id"
3212 if ($U->is_true($bib->deleted)) {
3213 my $holds = $mgr->editor->search_action_hold_request(
3214 { cancel_time => undef,
3215 fulfillment_time => undef,
3216 target => $li->eg_bib_id
3220 my %cached_usr_home_ou = ();
3222 for my $hold (@$holds) {
3224 $logger->info("Cancelling hold ".$hold->id.
3225 " due to acq lineitem cancellation.");
3227 $hold->cancel_time('now');
3228 $hold->cancel_cause(5); # 'Staff forced'--we may want a new hold cancel cause reason for this
3229 $hold->cancel_note('Corresponding Acquistion Lineitem/Purchase Order was cancelled.');
3230 unless($mgr->editor->update_action_hold_request($hold)) {
3231 my $evt = $mgr->editor->event;
3232 $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
3233 return new OpenILS::Event(
3234 "ACQ_NOT_CANCELABLE", "note" => "Could not cancel hold " . $hold->id . " for lineitem $li_id", "payload" => $evt
3237 if (! defined $mgr->{post_commit}) { # we need a mechanism for creating trigger events, but only if the transaction gets committed
3238 $mgr->{post_commit} = [];
3240 push @{ $mgr->{post_commit} }, sub {
3241 my $home_ou = $cached_usr_home_ou{$hold->usr};
3243 my $user = $mgr->editor->retrieve_actor_user($hold->usr); # FIXME: how do we want to handle failures here?
3244 $home_ou = $user->home_ou;
3245 $cached_usr_home_ou{$hold->usr} = $home_ou;
3247 $U->create_events_for_hook('hold_request.cancel.cancelled_order', $hold, $home_ou);
3253 update_lineitem($mgr, $li) or return 0;
3256 "state" => $li->state,
3257 "cancel_reason" => $cancel_reason
3261 # check to see if this cancelation should result in
3262 # marking the purchase order "received"
3263 return 0 unless check_purchase_order_received($mgr, $li->purchase_order->id);
3268 sub autocancel_lineitem {
3271 my $candidate_cancel_reason = shift;
3273 my $lid = $mgr->editor->search_acq_lineitem_detail({id => $lid_id});
3274 my $li_id = $lid->[0]->lineitem;
3276 my $all_lids = $mgr->editor->search_acq_lineitem_detail([{
3280 flesh_fields => { acqlid => ['cancel_reason'] }
3283 my $all_lids_are_canceled = 1;
3284 foreach my $lid ( @{ $all_lids } ) {
3285 if (! $lid->cancel_reason ) {
3286 $all_lids_are_canceled = 0;
3288 !$U->is_true($candidate_cancel_reason->keep_debits) &&
3289 $U->is_true($lid->cancel_reason->keep_debits)) {
3290 $candidate_cancel_reason = $lid->cancel_reason;
3294 if ($all_lids_are_canceled) {
3295 $cancel_result = cancel_lineitem($mgr, $li_id, $candidate_cancel_reason);
3297 return $cancel_result;
3300 __PACKAGE__->register_method(
3301 method => "cancel_lineitem_detail_api",
3302 api_name => "open-ils.acq.lineitem_detail.cancel",
3304 desc => q/Cancels an on-order lineitem detail/,
3306 {desc => "Authentication token", type => "string"},
3307 {desc => "Lineitem detail ID to cancel", type => "number"},
3308 {desc => "Cancel reason ID", type => "number"}
3310 return => {desc => q/Object describing changed LIDs on success;
3315 sub cancel_lineitem_detail_api {
3316 my ($self, $conn, $auth, $lid_id, $cancel_reason) = @_;
3318 my $e = new_editor("xact" => 1, "authtoken" => $auth);
3319 return $e->die_event unless $e->checkauth;
3320 my $mgr = new OpenILS::Application::Acq::BatchManager(
3321 "editor" => $e, "conn" => $conn
3324 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
3325 return new OpenILS::Event(
3326 "BAD_PARAMS", "note" => "Provide cancel reason ID"
3329 my $result = cancel_lineitem_detail($mgr, $lid_id, $cancel_reason) or
3330 return $e->die_event;
3332 if (not_cancelable($result)) {
3335 } elsif ($result == -1) {
3337 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3340 if (defined autocancel_lineitem($mgr,$lid_id,$cancel_reason)) {
3341 $$result{'li_update_needed'} = 1;
3344 $e->commit or return $e->die_event;
3346 # XXX create lineitem detail status events?
3350 sub cancel_lineitem_detail {
3351 my ($mgr, $lid_id, $cancel_reason) = @_;
3352 my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
3356 "acqlid" => ["lineitem","cancel_reason"],
3357 "jub" => ["purchase_order"]
3362 # It's OK to cancel an already-canceled copy if the copy was
3363 # previously "delayed" -- keep_debits == true
3364 # Depending on context, this may not warrant an event.
3365 return -1 if $lid->cancel_reason
3366 and $lid->cancel_reason->keep_debits eq 'f';
3368 # But this always does.
3369 return new OpenILS::Event(
3370 "ACQ_NOT_CANCELABLE", "note" => "lineitem_detail $lid_id"
3372 (! $lid->lineitem->purchase_order) or
3374 (not $lid->recv_time) and
3376 $lid->lineitem->purchase_order and (
3377 $lid->lineitem->state eq "on-order" or
3378 $lid->lineitem->state eq "pending-order" or
3379 $lid->lineitem->state eq "cancelled"
3384 return 0 unless $mgr->editor->allowed(
3385 "CREATE_PURCHASE_ORDER",
3386 $lid->lineitem->purchase_order->ordering_agency
3387 ) or (! $lid->lineitem->purchase_order);
3389 $lid->cancel_reason($cancel_reason->id);
3391 unless($U->is_true($cancel_reason->keep_debits)) {
3392 my $debit_id = $lid->fund_debit;
3393 $lid->clear_fund_debit;
3396 # item is cancelled. Remove the fund debit.
3397 my $debit = $mgr->editor->retrieve_acq_fund_debit($debit_id);
3398 if (!$U->is_true($debit->encumbrance)) {
3399 $mgr->editor->rollback;
3400 return OpenILS::Event->new('ACQ_NOT_CANCELABLE',
3401 note => "Debit is marked as paid: $debit_id");
3403 $mgr->editor->delete_acq_fund_debit($debit) or return $mgr->editor->die_event;
3407 # XXX LIDs don't have either an editor or a edit_time field. Should we
3408 # update these on the LI when we alter an LID?
3409 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
3411 return {"lid" => {$lid_id => {"cancel_reason" => $cancel_reason}}};
3414 __PACKAGE__->register_method(
3415 method => "delete_po_item_api",
3416 api_name => "open-ils.acq.po_item.delete",
3418 desc => q/Deletes a po_item and removes its debit/,
3420 {desc => "Authentication token", type => "string"},
3421 {desc => "po_item ID to delete", type => "number"},
3423 return => {desc => q/1 on success, Event on error/}
3427 sub delete_po_item_api {
3428 my($self, $client, $auth, $po_item_id) = @_;
3429 my $e = new_editor(authtoken => $auth, xact => 1);
3430 return $e->die_event unless $e->checkauth;
3432 my $po_item = $e->retrieve_acq_po_item([
3435 flesh_fields => {acqpoi => ['purchase_order', 'fund_debit']}
3437 ]) or return $e->die_event;
3439 return $e->die_event unless
3440 $e->allowed('CREATE_PURCHASE_ORDER',
3441 $po_item->purchase_order->ordering_agency);
3443 # remove debit, delete item
3444 my $result = clear_po_item($e, $po_item, 1);
3456 # 1. Removes linked fund debit from a PO item if present and still encumbered.
3457 # 2. Optionally also deletes the po_item object
3458 # po_item is fleshed with purchase_order and fund_debit
3460 my ($e, $po_item, $delete_item) = @_;
3462 if ($po_item->fund_debit) {
3464 if (!$U->is_true($po_item->fund_debit->encumbrance)) {
3465 # debit has been paid. We cannot delete it.
3466 return OpenILS::Event->new('ACQ_NOT_CANCELABLE',
3467 note => "Debit is marked as paid: ".$po_item->fund_debit->id);
3470 # fund_debit is OK to delete.
3471 $e->delete_acq_fund_debit($po_item->fund_debit)
3472 or return $e->die_event;
3476 $e->delete_acq_po_item($po_item) or return $e->die_event;
3478 # remove our link to the now-deleted fund_debit.
3479 $po_item->clear_fund_debit;
3480 $e->update_acq_po_item($po_item) or return $e->die_event;
3487 __PACKAGE__->register_method(
3488 method => 'user_requests',
3489 api_name => 'open-ils.acq.user_request.retrieve.by_user_id',
3492 desc => 'Retrieve fleshed user requests and related data for a given user.',
3494 { desc => 'Authentication token', type => 'string' },
3495 { desc => 'User ID of the owner, or array of IDs', },
3496 { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3501 desc => 'Fleshed user requests and related data',
3507 __PACKAGE__->register_method(
3508 method => 'user_requests',
3509 api_name => 'open-ils.acq.user_request.retrieve.by_home_ou',
3512 desc => 'Retrieve fleshed user requests and related data for a given org unit or units.',
3514 { desc => 'Authentication token', type => 'string' },
3515 { desc => 'Org unit ID, or array of IDs', },
3516 { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3521 desc => 'Fleshed user requests and related data',
3528 my($self, $conn, $auth, $search_value, $options) = @_;
3529 my $e = new_editor(authtoken => $auth);
3530 return $e->event unless $e->checkauth;
3531 my $rid = $e->requestor->id;
3535 "select"=>{"aur"=>["id"],"au"=>["home_ou", {column => 'id', alias => 'usr_id'} ]},
3536 "from"=>{ "aur" => { "au" => {}, "jub" => { "type" => "left" } } },
3540 {"id"=>undef}, # this with the left-join pulls in requests without lineitems
3541 {"state"=>["new","on-order","pending-order"]} # FIXME - probably needs softcoding
3545 "order_by"=>[{"class"=>"aur", "field"=>"request_date", "direction"=>"desc"}]
3548 foreach (qw/ order_by limit offset /) {
3549 $query->{$_} = $options->{$_} if defined $options->{$_};
3551 if (defined $options->{'state'}) {
3552 $query->{'where'}->{'+jub'}->{'-or'}->[1]->{'state'} = $options->{'state'};
3555 if ($self->api_name =~ /by_user_id/) {
3556 $query->{'where'}->{'usr'} = $search_value;
3558 $query->{'where'}->{'+au'} = { 'home_ou' => $search_value };
3561 my $pertinent_ids = $e->json_query($query);
3564 for my $id_blob (@$pertinent_ids) {
3565 if ($rid != $id_blob->{usr_id}) {
3566 if (!defined $perm_test{ $id_blob->{home_ou} }) {
3567 $perm_test{ $id_blob->{home_ou} } = $e->allowed( ['user_request.view'], $id_blob->{home_ou} );
3569 if (!$perm_test{ $id_blob->{home_ou} }) {
3573 my $aur_obj = $e->retrieve_acq_user_request([
3575 {flesh => 1, flesh_fields => { "aur" => [ 'lineitem' ] } }
3577 if (! $aur_obj) { next; }
3579 if ($aur_obj->lineitem()) {
3580 $aur_obj->lineitem()->clear_marc();
3582 $conn->respond($aur_obj);
3588 __PACKAGE__->register_method (
3589 method => 'update_user_request',
3590 api_name => 'open-ils.acq.user_request.cancel.batch',
3593 desc => 'If given a cancel reason, will update the request with that reason, otherwise, this will delete the request altogether. The ' .
3594 'intention is for staff interfaces or processes to provide cancel reasons, and for patron interfaces to just delete the requests.' ,
3596 { desc => 'Authentication token', type => 'string' },
3597 { desc => 'ID or array of IDs for the user requests to cancel' },
3598 { desc => 'Cancel Reason ID (optional)', type => 'string' }
3601 desc => 'progress object, event on error',
3605 __PACKAGE__->register_method (
3606 method => 'update_user_request',
3607 api_name => 'open-ils.acq.user_request.set_no_hold.batch',
3610 desc => 'Remove the hold from a user request or set of requests',
3612 { desc => 'Authentication token', type => 'string' },
3613 { desc => 'ID or array of IDs for the user requests to modify' }
3616 desc => 'progress object, event on error',
3620 __PACKAGE__->register_method (
3621 method => 'update_user_request',
3622 api_name => 'open-ils.acq.user_request.set_yes_hold.batch',
3625 desc => 'Set hold to true for a user request or set of requests',
3627 { desc => 'Authentication token', type => 'string' },
3628 { desc => 'ID or array of IDs for the user requests to modify' }
3631 desc => 'progress object, event on error',
3636 sub update_user_request {
3637 my($self, $conn, $auth, $aur_ids, $cancel_reason) = @_;
3638 my $e = new_editor(xact => 1, authtoken => $auth);
3639 return $e->die_event unless $e->checkauth;
3640 my $rid = $e->requestor->id;
3644 for my $id (@$aur_ids) {
3646 my $aur_obj = $e->retrieve_acq_user_request([
3649 flesh_fields => { "aur" => ['lineitem', 'usr'] }
3651 ]) or return $e->die_event;
3653 my $context_org = $aur_obj->usr()->home_ou();
3654 $aur_obj->usr( $aur_obj->usr()->id() );
3656 if ($rid != $aur_obj->usr) {
3657 if (!defined $perm_test{ $context_org }) {
3658 $perm_test{ $context_org } = $e->allowed( ['user_request.update'], $context_org );
3660 if (!$perm_test{ $context_org }) {
3665 if($self->api_name =~ /set_no_hold/) {
3666 if ($U->is_true($aur_obj->hold)) {
3667 $aur_obj->hold(0); # FIXME - this is not really removing holds per the description
3668 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3672 if($self->api_name =~ /set_yes_hold/) {
3673 if (!$U->is_true($aur_obj->hold)) {
3675 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3679 if($self->api_name =~ /cancel/) {
3680 if ( $cancel_reason ) {
3681 $aur_obj->cancel_reason( $cancel_reason );
3682 $aur_obj->cancel_time( 'now' );
3683 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3684 create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
3686 $e->delete_acq_user_request($aur_obj);
3690 $conn->respond({maximum => scalar(@$aur_ids), progress => $x++});
3694 return {complete => 1};
3697 __PACKAGE__->register_method (
3698 method => 'clear_completed_user_requests',
3699 api_name => 'open-ils.acq.clear_completed_user_requests',
3703 Auto-cancel the specified user requests if they are complete.
3704 Completed is defined as having either a Request Status of Fulfilled
3705 (which happens when the request is not Canceled and has an associated
3706 hold request that has a fulfillment time), or having a Request Status
3707 of Received (which happens when the request status is not Canceled or
3708 Fulfilled and has an associated Purchase Order with a State of
3709 Received) and a Place Hold value of False.
3712 { desc => 'Authentication token', type => 'string' },
3713 { desc => 'ID for home library of user requests to auto-cancel.' }
3716 desc => 'progress object, event on error',
3721 sub clear_completed_user_requests {
3722 my($self, $conn, $auth, $potential_aur_ids) = @_;
3723 my $e = new_editor(xact => 1, authtoken => $auth);
3724 return $e->die_event unless $e->checkauth;
3725 my $rid = $e->requestor->id;
3727 my $potential_requests = $e->search_acq_user_request_status({
3728 id => $potential_aur_ids
3730 { request_status => 6 }, # Fulfilled
3731 { '-and' => [ { request_status => 5 }, { hold => 'f' } ] } # Received
3737 my %perm_test = (); my %perm_test2 = ();
3738 for my $request (@$potential_requests) {
3739 if ($rid != $request->usr()) {
3740 if (!defined $perm_test{ $request->home_ou() }) {
3741 $perm_test{ $request->home_ou() } =
3742 $e->allowed( ['user_request.view'], $request->home_ou() );
3744 if (!defined $perm_test2{ $request->home_ou() }) {
3745 $perm_test2{ $request->home_ou() } =
3746 $e->allowed( ['CLEAR_PURCHASE_REQUEST'], $request->home_ou() );
3748 if (!$perm_test{ $request->home_ou() }) {
3751 if (!$perm_test2{ $request->home_ou() }) {
3755 push @$aur_ids, $request->id();
3759 my %perm_test3 = ();
3760 for my $id (@$aur_ids) {
3762 my $aur_obj = $e->retrieve_acq_user_request([
3765 flesh_fields => { "aur" => ['lineitem', 'usr'] }
3767 ]) or return $e->die_event;
3769 my $context_org = $aur_obj->usr()->home_ou();
3770 $aur_obj->usr( $aur_obj->usr()->id() );
3772 if ($rid != $aur_obj->usr) {
3773 if (!defined $perm_test3{ $context_org }) {
3774 $perm_test3{ $context_org } = $e->allowed( ['user_request.update'], $context_org );
3776 if (!$perm_test3{ $context_org }) {
3781 $aur_obj->cancel_reason( 1015 ); # Canceled: Fulfilled
3782 $aur_obj->cancel_time( 'now' );
3783 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3784 create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
3785 # FIXME - hrmm, since this is a special type of "cancelation", should we not fire these
3786 # events or should we put the burden on A/T to filter things based on cancel_reason if
3787 # desired? I don't think anyone is actually using A/T for these in practice
3789 $conn->respond({maximum => scalar(@$aur_ids), progress => $x++});
3793 return {complete => 1};
3796 __PACKAGE__->register_method (
3797 method => 'new_user_request',
3798 api_name => 'open-ils.acq.user_request.create',
3800 desc => 'Create a new user request object in the DB',
3802 { desc => 'Authentication token', type => 'string' },
3803 { desc => 'User request data hash. Hash keys match the fields for the "aur" object', type => 'object' }
3806 desc => 'The created user request object, or event on error'
3811 sub new_user_request {
3812 my($self, $conn, $auth, $form_data) = @_;
3813 my $e = new_editor(xact => 1, authtoken => $auth);
3814 return $e->die_event unless $e->checkauth;
3815 my $rid = $e->requestor->id;
3816 my $target_user_fleshed;
3817 if (! defined $$form_data{'usr'}) {
3818 $$form_data{'usr'} = $rid;
3820 if ($$form_data{'usr'} != $rid) {
3821 # See if the requestor can place the request on behalf of a different user.
3822 $target_user_fleshed = $e->retrieve_actor_user($$form_data{'usr'}) or return $e->die_event;
3823 $e->allowed('user_request.create', $target_user_fleshed->home_ou) or return $e->die_event;
3825 $target_user_fleshed = $e->requestor;
3826 $e->allowed('CREATE_PURCHASE_REQUEST') or return $e->die_event;
3828 if (! defined $$form_data{'pickup_lib'}) {
3829 if ($target_user_fleshed->ws_ou) {
3830 $$form_data{'pickup_lib'} = $target_user_fleshed->ws_ou;
3832 $$form_data{'pickup_lib'} = $target_user_fleshed->home_ou;
3835 if (! defined $$form_data{'request_type'}) {
3836 $$form_data{'request_type'} = 1; # Books
3838 my $aur_obj = new Fieldmapper::acq::user_request;
3840 $aur_obj->usr( $$form_data{'usr'} );
3841 $aur_obj->request_date( 'now' );
3842 for my $field ( keys %$form_data ) {
3843 if (defined $$form_data{$field} and $field !~ /^(id|lineitem|eg_bib|request_date|cancel_reason)$/) {
3844 $aur_obj->$field( $$form_data{$field} );
3848 $aur_obj = $e->create_acq_user_request($aur_obj) or return $e->die_event;
3850 $e->commit and create_user_request_events( $e, [ $aur_obj ], 'aur.created' );
3855 sub create_user_request_events {
3856 my($e, $user_reqs, $hook) = @_;
3858 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3861 my %cached_usr_home_ou = ();
3862 for my $user_req (@$user_reqs) {
3863 my $home_ou = $cached_usr_home_ou{$user_req->usr};
3865 my $user = $e->retrieve_actor_user($user_req->usr) or return $e->die_event;
3866 $home_ou = $user->home_ou;
3867 $cached_usr_home_ou{$user_req->usr} = $home_ou;
3869 my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $home_ou);
3878 __PACKAGE__->register_method(
3879 method => "po_note_CUD_batch",
3880 api_name => "open-ils.acq.po_note.cud.batch",
3883 desc => q/Manage purchase order notes/,
3885 {desc => "Authentication token", type => "string"},
3886 {desc => "List of po_notes to manage", type => "array"},
3888 return => {desc => "Stream of successfully managed objects"}
3892 sub po_note_CUD_batch {
3893 my ($self, $conn, $auth, $notes) = @_;
3895 my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3896 return $e->die_event unless $e->checkauth;
3899 my $total = @$notes;
3902 foreach my $note (@$notes) {
3904 $note->editor($e->requestor->id);
3905 $note->edit_time("now");
3908 $note->creator($e->requestor->id);
3909 $note = $e->create_acq_po_note($note) or return $e->die_event;
3910 } elsif ($note->isdeleted) {
3911 $e->delete_acq_po_note($note) or return $e->die_event;
3912 } elsif ($note->ischanged) {
3913 $e->update_acq_po_note($note) or return $e->die_event;
3916 unless ($note->isdeleted) {
3917 $note = $e->retrieve_acq_po_note($note->id) or
3918 return $e->die_event;
3922 {"maximum" => $total, "progress" => ++$count, "note" => $note}
3926 $e->commit and $conn->respond_complete or return $e->die_event;
3930 # retrieves a lineitem, fleshes its PO and PL, checks perms
3931 # returns ($li, $evt, $org)
3932 sub fetch_and_check_li {
3935 my $perm_mode = shift || 'read';
3937 my $li = $e->retrieve_acq_lineitem([
3940 flesh_fields => {jub => ['purchase_order', 'picklist']}
3942 ]) or return (undef, $e->die_event);
3945 if(my $po = $li->purchase_order) {
3946 $org = $po->ordering_agency;
3947 my $perms = ($perm_mode eq 'read') ? 'VIEW_PURCHASE_ORDER' : 'CREATE_PURCHASE_ORDER';
3948 return ($li, $e->die_event) unless $e->allowed($perms, $org);
3950 } elsif(my $pl = $li->picklist) {
3951 $org = $pl->org_unit;
3952 my $perms = ($perm_mode eq 'read') ? 'VIEW_PICKLIST' : 'CREATE_PICKLIST';
3953 return ($li, $e->die_event) unless $e->allowed($perms, $org);
3956 return ($li, undef, $org);
3960 __PACKAGE__->register_method(
3961 method => "clone_distrib_form",
3962 api_name => "open-ils.acq.distribution_formula.clone",
3965 desc => q/Clone a distribution formula/,
3967 {desc => "Authentication token", type => "string"},
3968 {desc => "Original formula ID", type => 'integer'},
3969 {desc => "Name of new formula", type => 'string'},
3971 return => {desc => "ID of newly created formula"}
3975 sub clone_distrib_form {
3976 my($self, $client, $auth, $form_id, $new_name) = @_;
3978 my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3979 return $e->die_event unless $e->checkauth;
3981 my $old_form = $e->retrieve_acq_distribution_formula($form_id) or return $e->die_event;
3982 return $e->die_event unless $e->allowed('ADMIN_ACQ_DISTRIB_FORMULA', $old_form->owner);
3984 my $new_form = Fieldmapper::acq::distribution_formula->new;
3986 $new_form->owner($old_form->owner);
3987 $new_form->name($new_name);
3988 $e->create_acq_distribution_formula($new_form) or return $e->die_event;
3990 my $entries = $e->search_acq_distribution_formula_entry({formula => $form_id});
3991 for my $entry (@$entries) {
3992 my $new_entry = Fieldmapper::acq::distribution_formula_entry->new;
3993 $new_entry->$_($entry->$_()) for $entry->real_fields;
3994 $new_entry->formula($new_form->id);
3995 $new_entry->clear_id;
3996 $e->create_acq_distribution_formula_entry($new_entry) or return $e->die_event;
4000 return $new_form->id;
4003 __PACKAGE__->register_method(
4004 method => 'add_li_to_po',
4005 api_name => 'open-ils.acq.purchase_order.add_lineitem',
4007 desc => q/Adds a lineitem to an existing purchase order/,
4009 {desc => 'Authentication token', type => 'string'},
4010 {desc => 'The purchase order id', type => 'number'},
4011 {desc => 'The lineitem ID (or an array of them)', type => 'mixed'},
4013 return => {desc => 'Streams a total versus completed counts object, event on error'}
4018 my($self, $conn, $auth, $po_id, $li_id) = @_;
4020 my $e = new_editor(authtoken => $auth, xact => 1);
4021 return $e->die_event unless $e->checkauth;
4023 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
4025 my $po = $e->retrieve_acq_purchase_order($po_id)
4026 or return $e->die_event;
4028 return $e->die_event unless
4029 $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
4031 unless ($po->state =~ /new|pending/) {
4033 return {success => 0, po => $po, error => 'bad-po-state'};
4038 if (ref $li_id eq "ARRAY") {
4039 $li_id = [ map { int($_) } @$li_id ];
4040 return $e->die_event(new OpenILS::Event("BAD_PARAMS")) unless @$li_id;
4042 $lis = $e->search_acq_lineitem({id => $li_id})
4043 or return $e->die_event;
4045 my $li = $e->retrieve_acq_lineitem(int($li_id))
4046 or return $e->die_event;
4050 foreach my $li (@$lis) {
4051 if ($li->state !~ /new|order-ready|pending-order/ or
4052 $li->purchase_order) {
4054 return {success => 0, li => $li, error => 'bad-li-state'};
4057 $li->provider($po->provider);
4058 $li->purchase_order($po_id);
4059 $li->state('pending-order');
4060 apply_default_copies($mgr, $po, $li->id) or return $e->die_event;
4061 update_lineitem($mgr, $li) or return $e->die_event;
4065 return {success => 1};
4068 __PACKAGE__->register_method(
4069 method => 'po_lineitems_no_copies',
4070 api_name => 'open-ils.acq.purchase_order.no_copy_lineitems.id_list',
4074 desc => q/Returns the set of lineitem IDs for a given PO that have no copies attached/,
4076 {desc => 'Authentication token', type => 'string'},
4077 {desc => 'The purchase order id', type => 'number'},
4079 return => {desc => 'Stream of lineitem IDs on success, event on error'}
4083 sub po_lineitems_no_copies {
4084 my ($self, $conn, $auth, $po_id) = @_;
4086 my $e = new_editor(authtoken => $auth);
4087 return $e->event unless $e->checkauth;
4089 # first check the view perms for LI's attached to this PO
4090 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
4091 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
4093 my $ids = $e->json_query({
4094 select => {jub => ['id']},
4095 from => {jub => {acqlid => {type => 'left'}}},
4097 '+jub' => {purchase_order => $po_id},
4098 '+acqlid' => {lineitem => undef}
4102 $conn->respond($_->{id}) for @$ids;
4106 __PACKAGE__->register_method(
4107 method => 'set_li_order_ident',
4108 api_name => 'open-ils.acq.lineitem.order_identifier.set',
4111 Given an existing lineitem_attr (typically a marc_attr), this will
4112 create a matching local_attr to store the name and value and mark
4113 the attr as the order_ident. Any existing local_attr marked as
4114 order_ident is removed.
4117 {desc => 'Authentication token', type => 'string'},
4118 {desc => q/Args object:
4119 source_attr_id : ID of the existing lineitem_attr to use as
4121 lineitem_id : lineitem id
4122 attr_name : name ('isbn', etc.) of a new marc_attr to add to
4123 the lineitem to use for the order ident
4124 attr_value : value for the new marc_attr
4125 no_apply_bre : if set, newly added attrs will not be applied
4126 to the lineitems' linked bib record/,
4129 return => {desc => q/Returns the attribute
4130 responsible for tracking the order identifier/}
4134 sub set_li_order_ident {
4135 my ($self, $conn, $auth, $args) = @_;
4139 my $source_attr_id = $args->{source_attr_id};
4141 my $e = new_editor(authtoken => $auth, xact => 1);
4142 return $e->die_event unless $e->checkauth;
4144 # fetch attr, LI, and check update permissions
4146 my $li_id = $args->{lineitem_id};
4148 if ($source_attr_id) {
4149 $source_attr = $e->retrieve_acq_lineitem_attr($source_attr_id)
4150 or return $e->die_event;
4151 $li_id = $source_attr->lineitem;
4154 my ($li, $evt, $perm_org) = fetch_and_check_li($e, $li_id, 'write');
4155 return $evt if $evt;
4157 return $e->die_event unless
4158 $e->allowed('ACQ_SET_LINEITEM_IDENTIFIER', $perm_org);
4160 # if needed, create a new marc attr for
4161 # the lineitem to represent the ident value
4163 ($source_attr, $evt) = apply_new_li_ident_attr(
4164 $e, $li, $perm_org, $args->{attr_name}, $args->{attr_value})
4165 unless $source_attr;
4167 return $evt if $evt;
4169 # remove the existing order_ident attribute if present
4171 my $old_attr = $e->search_acq_lineitem_attr({
4172 attr_type => 'lineitem_local_attr_definition',
4173 lineitem => $li->id,
4179 # if we already have an order_ident that matches the
4180 # source attr, there's nothing left to do.
4182 if ($old_attr->attr_name eq $source_attr->attr_name and
4183 $old_attr->attr_value eq $source_attr->attr_value) {
4189 # remove the old order_ident attribute
4190 $e->delete_acq_lineitem_attr($old_attr) or return $e->die_event;
4194 # make sure we have a local_attr_def to match the source attr def
4196 my $local_def = $e->search_acq_lineitem_local_attr_definition({
4197 code => $source_attr->attr_name
4202 $e->retrieve_acq_lineitem_attr_definition($source_attr->definition);
4203 $local_def = Fieldmapper::acq::lineitem_local_attr_definition->new;
4204 $local_def->code($source_def->code);
4205 $local_def->description($source_def->description);
4206 $local_def = $e->create_acq_lineitem_local_attr_definition($local_def)
4207 or return $e->die_event;
4210 # create the new order_ident local attr
4212 my $new_attr = Fieldmapper::acq::lineitem_attr->new;
4213 $new_attr->definition($local_def->id);
4214 $new_attr->attr_type('lineitem_local_attr_definition');
4215 $new_attr->lineitem($li->id);
4216 $new_attr->attr_name($source_attr->attr_name);
4217 $new_attr->attr_value($source_attr->attr_value);
4218 $new_attr->order_ident('t');
4220 $new_attr = $e->create_acq_lineitem_attr($new_attr)
4221 or return $e->die_event;
4228 # Given an isbn, issn, or upc, add the value to the lineitem marc.
4229 # Upon update, the value will be auto-magically represented as
4230 # a lineitem marc attr.
4231 # If the li is linked to a bib record and the user has the correct
4232 # permissions, update the bib record to match.
4233 sub apply_new_li_ident_attr {
4234 my ($e, $li, $perm_org, $attr_name, $attr_value) = @_;
4242 my $marc_field = MARC::Field->new(
4243 $tags{$attr_name}, '', '','a' => $attr_value);
4245 my $li_rec = MARC::Record->new_from_xml($li->marc, 'UTF-8', 'USMARC');
4246 $li_rec->insert_fields_ordered($marc_field);
4248 $li->marc(clean_marc($li_rec));
4249 $li->editor($e->requestor->id);
4250 $li->edit_time('now');
4252 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
4254 my $source_attr = $e->search_acq_lineitem_attr({
4255 attr_name => $attr_name,
4256 attr_value => $attr_value,
4257 attr_type => 'lineitem_marc_attr_definition'
4260 if (!$source_attr) {
4261 $logger->error("ACQ lineitem update failed to produce a matching ".
4262 " marc attribute for $attr_name => $attr_value");
4263 return (undef, OpenILS::Event->new('INTERNAL_SERVER_ERROR'));
4266 return ($source_attr) unless
4268 $e->allowed('ACQ_ADD_LINEITEM_IDENTIFIER', $perm_org);
4270 # li is linked to a bib record and user has the update perms
4272 my $bre = $e->retrieve_biblio_record_entry($li->eg_bib_id);
4273 my $bre_marc = MARC::Record->new_from_xml($bre->marc, 'UTF-8', 'USMARC');
4274 $bre_marc->insert_fields_ordered($marc_field);
4276 $bre->marc(clean_marc($bre_marc));
4277 $bre->editor($e->requestor->id);
4278 $bre->edit_date('now');
4280 $e->update_biblio_record_entry($bre) or return (undef, $e->die_event);
4282 return ($source_attr);
4285 __PACKAGE__->register_method(
4286 method => 'li_existing_copies',
4287 api_name => 'open-ils.acq.lineitem.existing_copies.count',
4291 Returns the number of catalog copies (acp) which are children of
4292 the same bib record linked to by the given lineitem and which
4293 are owned at or below the lineitem context org unit.
4294 Copies with the following statuses are not counted:
4295 Lost, Missing, Discard Weed, and Lost and Paid.
4298 {desc => 'Authentication token', type => 'string'},
4299 {desc => 'Lineitem ID', type => 'number'}
4301 return => {desc => q/Count or event on error/}
4305 sub li_existing_copies {
4306 my ($self, $client, $auth, $li_id) = @_;
4307 my $e = new_editor("authtoken" => $auth);
4308 return $e->die_event unless $e->checkauth;
4310 my ($li, $evt, $org) = fetch_and_check_li($e, $li_id);
4313 # No fuzzy matching here (e.g. on ISBN). Only exact matches are supported.
4314 return 0 unless $li->eg_bib_id;
4316 my $counts = $e->json_query({
4317 select => {acp => [{
4319 transform => 'count',
4326 field => 'eg_copy_id',
4329 acn => {join => {bre => {}}}
4333 '+bre' => {id => $li->eg_bib_id},
4334 # don't count copies linked to the lineitem in question
4337 {lineitem => undef},
4338 {lineitem => {'<>' => $li_id}}
4342 owning_lib => $U->get_org_descendants($org)
4344 # NOTE: should the excluded copy statuses be an AOUS?
4345 '+acp' => {status => {'not in' => [3, 4, 13, 17]}}
4349 return $counts->[0]->{id};