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 $hold = Fieldmapper::action::hold_request->new;
269 $hold->usr( $request->usr );
270 $hold->requestor( $request->usr );
271 $hold->request_time( $request->request_date );
272 $hold->pickup_lib( $request->pickup_lib );
273 $hold->request_lib( $request->pickup_lib );
274 $hold->selection_ou( $request->pickup_lib );
275 $hold->phone_notify( $request->phone_notify );
276 $hold->email_notify( $request->email_notify );
277 $hold->expire_time( $request->need_before );
279 if ($request->holdable_formats) {
280 my $mrm = $mgr->editor->search_metabib_metarecord_source_map( { source => $li->eg_bib_id } )->[0];
282 $hold->hold_type( 'M' );
283 $hold->holdable_formats( $request->holdable_formats );
284 $hold->target( $mrm->metarecord );
288 if (!$hold->target) {
289 $hold->hold_type( 'T' );
290 $hold->target( $li->eg_bib_id );
293 # if behind-the-desk holds are supported at the
294 # pickup library, apply the patron default
295 my $bdous = $U->ou_ancestor_setting_value(
297 'circ.holds.behind_desk_pickup_supported',
302 my $set = $mgr->editor->search_actor_user_setting(
303 {usr => $hold->usr, name => 'circ.holds_behind_desk'})->[0];
305 $hold->behind_desk('t') if $set and
306 OpenSRF::Utils::JSON->JSON2perl($set->value);
309 $mgr->editor->create_action_hold_request( $hold ) or return 0;
315 sub delete_lineitem {
317 $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
319 # delete the attached lineitem_details
320 my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
321 for my $lid_id (@$lid_ids) {
322 return 0 unless delete_lineitem_detail($mgr, $lid_id);
326 return $mgr->editor->delete_acq_lineitem($li);
329 # begins and commit transactions as it goes
330 # bib_only exits before creation of copies and callnumbers
331 sub create_lineitem_list_assets {
332 my($mgr, $li_ids, $vandelay, $bib_only) = @_;
334 # Do not create line items if none are specified
335 return {} unless (scalar(@$li_ids));
337 if (check_import_li_marc_perms($mgr, $li_ids)) { # event on error
338 $logger->error("acq-vl: user does not have permission to import acq records");
342 my $res = import_li_bibs_via_vandelay($mgr, $li_ids, $vandelay);
343 return undef unless $res;
344 return $res if $bib_only;
346 # create the bibs/volumes/copies for the successfully imported records
347 for my $li_id (@{$res->{li_ids}}) {
348 $mgr->editor->xact_begin;
349 my $data = create_lineitem_assets($mgr, $li_id) or return undef;
350 $mgr->editor->xact_commit;
357 sub test_vandelay_import_args {
358 my $vandelay = shift;
359 my $q_needed = shift;
361 # we need valid args and (sometimes) a queue
362 return 0 unless $vandelay and (
364 $vandelay->{queue_name} or
365 $vandelay->{existing_queue}
368 # match-based merge/overlay import
369 return 2 if $vandelay->{merge_profile} and (
370 $vandelay->{auto_overlay_exact} or
371 $vandelay->{auto_overlay_1match} or
372 $vandelay->{auto_overlay_best_match}
376 return 2 if $vandelay->{import_no_match};
378 return 1; # queue only
381 sub find_or_create_vandelay_queue {
382 my ($e, $vandelay) = @_;
385 if (my $name = $vandelay->{queue_name}) {
387 # first, see if a queue w/ this name already exists
388 # for this user. If so, use that instead.
390 $queue = $e->search_vandelay_bib_queue(
391 {name => $name, owner => $e->requestor->id})->[0];
395 $logger->info("acq-vl: using existing queue $name");
399 $logger->info("acq-vl: creating new vandelay queue $name");
401 $queue = new Fieldmapper::vandelay::bib_queue;
403 $queue->queue_type('acq');
404 $queue->owner($e->requestor->id);
405 $queue->match_set($vandelay->{match_set} || undef); # avoid ''
406 $queue = $e->create_vandelay_bib_queue($queue) or return undef;
410 $queue = $e->retrieve_vandelay_bib_queue($vandelay->{existing_queue})
418 sub import_li_bibs_via_vandelay {
419 my ($mgr, $li_ids, $vandelay) = @_;
420 my $res = {li_ids => []};
421 my $e = $mgr->editor;
424 my $needs_importing = $e->search_acq_lineitem(
425 {id => $li_ids, eg_bib_id => undef},
429 if (!@$needs_importing) {
430 $logger->info("acq-vl: all records already imported. no Vandelay work to do");
431 return {li_ids => $li_ids};
434 # see if we have any records that are not yet linked to VL records (i.e.
435 # not in a queue). This will tell us if lack of a queue name is an error.
436 my $non_queued = $e->search_acq_lineitem(
437 {id => $needs_importing, queued_record => undef},
441 # add the already-imported records to the response list
442 push(@{$res->{li_ids}}, grep { $_ != @$needs_importing } @$li_ids);
444 $logger->info("acq-vl: processing recs via Vandelay with args: ".Dumper($vandelay));
446 my $vl_stat = test_vandelay_import_args($vandelay, scalar(@$non_queued));
448 $logger->error("acq-vl: invalid vandelay arguments for acq import (queue needed)");
454 # when any non-queued lineitems exist, their vandelay counterparts
455 # require a place to live.
456 $queue = find_or_create_vandelay_queue($e, $vandelay) or return $res;
459 # if all lineitems are already queued, the queue reported to the user
460 # is purely for information / convenience. pick a random queue.
461 $queue = $e->retrieve_acq_lineitem([
462 $needs_importing->[0], {
465 jub => ['queued_record'],
469 ])->queued_record->queue;
472 $mgr->{args}->{queue} = $queue;
474 # load the lineitems into the queue for merge processing
477 for my $li_id (@$needs_importing) {
479 my $li = $e->retrieve_acq_lineitem($li_id) or return $res;
481 if ($li->queued_record) {
482 $logger->info("acq-vl: $li_id already linked to a vandelay record");
483 push(@vqbr_ids, $li->queued_record);
486 $logger->info("acq-vl: creating new vandelay record for lineitem $li_id");
488 # create a new VL queued record and link it up
489 my $vqbr = Fieldmapper::vandelay::queued_bib_record->new;
490 $vqbr->marc($li->marc);
491 $vqbr->queue($queue->id);
492 $vqbr->bib_source($vandelay->{bib_source} || undef); # avoid ''
493 $vqbr = $e->create_vandelay_queued_bib_record($vqbr) or return $res;
494 push(@vqbr_ids, $vqbr->id);
496 # tell the acq record which vandelay record it's linked to
497 $li->queued_record($vqbr->id);
498 $e->update_acq_lineitem($li) or return $res;
506 $logger->info("acq-vl: created vandelay records [@vqbr_ids]");
508 # we have to commit the transaction now since
509 # vandelay uses its own transactions.
512 return $res if $vl_stat == 1; # queue only
514 # Import the bibs via vandelay. Note: Vandely will
515 # update acq.lineitem.eg_bib_id on successful import.
517 $vandelay->{report_all} = 1;
518 my $ses = OpenSRF::AppSession->create('open-ils.vandelay');
519 my $req = $ses->request(
520 'open-ils.vandelay.bib_record.list.import',
521 $e->authtoken, \@vqbr_ids, $vandelay);
523 # pull the responses, noting all that were successfully imported
525 while (my $resp = $req->recv(timeout => 600)) {
526 my $stat = $resp->content;
528 if(!$stat or $U->event_code($stat)) { # import failure
529 $logger->error("acq-vl: error importing vandelay record " . Dumper($stat));
533 # "imported" refers to the vqbr id, not the
534 # success/failure of the vqbr merge attempt
535 next unless $stat->{imported};
537 my ($imported) = grep {$_->queued_record eq $stat->{imported}} @lis;
538 my $li_id = $imported->id;
540 if ($stat->{no_import}) {
541 $logger->info("acq-vl: acq lineitem $li_id did not import");
543 } else { # successful import
545 push(@success_lis, $li_id);
548 $logger->info("acq-vl: acq lineitem $li_id successfully merged/imported");
553 $logger->info("acq-vl: successfully imported lineitems [@success_lis]");
555 # add the successfully imported lineitems to the already-imported lineitems
556 push (@{$res->{li_ids}}, @success_lis);
561 # returns event on error, undef on success
562 sub check_import_li_marc_perms {
563 my($mgr, $li_ids) = @_;
565 # if there are any order records that are not linked to
566 # in-db bib records, verify staff has perms to import order records
567 my $order_li = $mgr->editor->search_acq_lineitem(
568 [{id => $li_ids, eg_bib_id => undef}, {limit => 1}], {idlist => 1})->[0];
571 return $mgr->editor->die_event unless
572 $mgr->editor->allowed('IMPORT_ACQ_LINEITEM_BIB_RECORD');
579 # ----------------------------------------------------------------------------
580 # if all of the lineitem details for this lineitem have
581 # been received, mark the lineitem as received
582 # returns 1 on non-received, li on received, 0 on error
583 # ----------------------------------------------------------------------------
585 sub describe_affected_po {
588 my ($enc, $spent, $estimated) =
589 OpenILS::Application::Acq::Financials::build_price_summary(
594 "state" => $po->state,
595 "amount_encumbered" => $enc,
596 "amount_spent" => $spent,
597 "amount_estimated" => $estimated
602 sub check_lineitem_received {
603 my($mgr, $li_id) = @_;
605 my $non_recv = $mgr->editor->search_acq_lineitem_detail(
606 {recv_time => undef, lineitem => $li_id}, {idlist=>1});
608 return 1 if @$non_recv;
610 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
611 $li->state('received');
612 return update_lineitem($mgr, $li);
615 sub receive_lineitem {
616 my($mgr, $li_id, $skip_complete_check) = @_;
617 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
619 return 0 unless $li->state eq 'on-order' or $li->state eq 'cancelled'; # sic
621 $li->clear_cancel_reason; # un-cancel on receive
623 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
624 {lineitem => $li_id, recv_time => undef}, {idlist => 1});
626 for my $lid_id (@$lid_ids) {
627 receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
631 $li->state('received');
633 $li = update_lineitem($mgr, $li) or return 0;
634 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li_id, 'aur.received'); });
638 $skip_complete_check or (
639 $po = check_purchase_order_received($mgr, $li->purchase_order)
642 my $result = {"li" => {$li->id => {"state" => $li->state}}};
643 $result->{"po"} = describe_affected_po($mgr->editor, $po) if ref $po;
647 sub rollback_receive_lineitem {
648 my($mgr, $li_id) = @_;
649 my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
651 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
652 {lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
654 for my $lid_id (@$lid_ids) {
655 rollback_receive_lineitem_detail($mgr, $lid_id, 1) or return 0;
659 $li->state('on-order');
660 return update_lineitem($mgr, $li);
664 sub create_lineitem_status_events {
665 my($mgr, $li_id, $hook) = @_;
667 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
669 my $user_reqs = $mgr->editor->search_acq_user_request([
670 {lineitem => $li_id},
671 {flesh => 1, flesh_fields => {aur => ['usr']}}
674 for my $user_req (@$user_reqs) {
675 my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $user_req->usr->home_ou);
683 # ----------------------------------------------------------------------------
685 # ----------------------------------------------------------------------------
686 sub create_lineitem_detail {
687 my($mgr, %args) = @_;
688 my $lid = Fieldmapper::acq::lineitem_detail->new;
689 $lid->$_($args{$_}) for keys %args;
692 return $mgr->editor->create_acq_lineitem_detail($lid);
696 # flesh out any required data with default values where appropriate
697 sub complete_lineitem_detail {
699 unless($lid->barcode) {
700 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
701 $lid->barcode($pfx.$lid->id);
704 unless($lid->cn_label) {
705 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
706 $lid->cn_label($pfx.$lid->id);
709 if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
710 $lid->location($loc);
713 $lid->circ_modifier(get_default_circ_modifier($mgr, $lid->owning_lib))
714 unless defined $lid->circ_modifier;
716 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
720 sub get_default_circ_modifier {
722 my $code = $mgr->cache($org, 'def_circ_mod');
723 $code = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier') unless defined $code;
724 return $mgr->cache($org, 'def_circ_mod', $code) if defined $code;
728 sub delete_lineitem_detail {
730 $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
731 return $mgr->editor->delete_acq_lineitem_detail($lid);
735 sub receive_lineitem_detail {
736 my($mgr, $lid_id, $skip_complete_check) = @_;
737 my $e = $mgr->editor;
739 my $lid = $e->retrieve_acq_lineitem_detail([
743 acqlid => ['fund_debit']
748 return 1 if $lid->recv_time;
750 # if the LID is marked as canceled, remove the cancel reason,
751 # and reinstate fund debits where deleted by cancelation.
752 if ($lid->cancel_reason) {
753 my $cr = $e->retrieve_acq_cancel_reason($lid->cancel_reason);
755 if (!$U->is_true($cr->keep_debits)) {
756 # debits were removed during cancelation.
757 create_lineitem_detail_debit(
758 $mgr, $lid->lineitem, $lid) or return 0;
760 $lid->clear_cancel_reason;
763 $lid->receiver($e->requestor->id);
764 $lid->recv_time('now');
765 $e->update_acq_lineitem_detail($lid) or return 0;
767 if ($lid->eg_copy_id) {
768 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
769 # only update status if it hasn't already been updated
770 $copy->status(OILS_COPY_STATUS_IN_PROCESS) if $copy->status == OILS_COPY_STATUS_ON_ORDER;
771 $copy->edit_date('now');
772 $copy->editor($e->requestor->id);
773 $copy->creator($e->requestor->id) if $U->ou_ancestor_setting_value(
774 $e->requestor->ws_ou, 'acq.copy_creator_uses_receiver', $e);
775 $e->update_asset_copy($copy) or return 0;
780 return 1 if $skip_complete_check;
782 my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
783 return 1 if $li == 1; # li not received
785 return check_purchase_order_received($mgr, $li->purchase_order) or return 0;
789 sub rollback_receive_lineitem_detail {
790 my($mgr, $lid_id) = @_;
791 my $e = $mgr->editor;
793 my $lid = $e->retrieve_acq_lineitem_detail([
797 acqlid => ['fund_debit']
802 return 1 unless $lid->recv_time;
804 $lid->clear_receiver;
805 $lid->clear_recv_time;
806 $e->update_acq_lineitem_detail($lid) or return 0;
808 if ($lid->eg_copy_id) {
809 my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
810 $copy->status(OILS_COPY_STATUS_ON_ORDER);
811 $copy->edit_date('now');
812 $copy->editor($e->requestor->id);
813 $e->update_asset_copy($copy) or return 0;
820 # ----------------------------------------------------------------------------
822 # ----------------------------------------------------------------------------
823 sub set_lineitem_attr {
824 my($mgr, %args) = @_;
825 my $attr_type = $args{attr_type};
827 # first, see if it's already set. May just need to overwrite it
828 my $attr = $mgr->editor->search_acq_lineitem_attr({
829 lineitem => $args{lineitem},
830 attr_type => $args{attr_type},
831 attr_name => $args{attr_name}
835 $attr->attr_value($args{attr_value});
836 return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
841 $attr = Fieldmapper::acq::lineitem_attr->new;
842 $attr->$_($args{$_}) for keys %args;
844 unless($attr->definition) {
845 my $find = "search_acq_$attr_type";
846 my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
847 $attr->definition($attr_def_id);
849 return $mgr->editor->create_acq_lineitem_attr($attr);
853 # ----------------------------------------------------------------------------
855 # ----------------------------------------------------------------------------
856 sub create_lineitem_debits {
857 my ($mgr, $li, $options) = @_;
859 my $dry_run = $options->{dry_run};
861 unless($li->estimated_unit_price) {
862 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
863 $mgr->editor->rollback;
867 unless($li->provider) {
868 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
869 $mgr->editor->rollback;
873 my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
874 {lineitem => $li->id},
878 if (@$lid_ids == 0 and !$options->{zero_copy_activate}) {
879 $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_COPIES', payload => $li->id));
880 $mgr->editor->rollback;
884 for my $lid_id (@$lid_ids) {
886 my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
889 flesh_fields => {acqlid => ['fund']}
893 create_lineitem_detail_debit($mgr, $li, $lid, $dry_run) or return 0;
902 sub create_lineitem_detail_debit {
903 my ($mgr, $li, $lid, $dry_run, $no_translate) = @_;
905 # don't create the debit if one already exists
906 return $mgr->editor->retrieve_acq_fund_debit($lid->fund_debit) if $lid->fund_debit;
908 my $li_id = ref($li) ? $li->id : $li;
910 unless(ref $li and ref $li->provider) {
911 $li = $mgr->editor->retrieve_acq_lineitem([
914 flesh_fields => {jub => ['provider']},
920 $lid->fund($mgr->editor->retrieve_acq_fund($lid->fund)) unless(ref $lid->fund);
922 $lid = $mgr->editor->retrieve_acq_lineitem_detail([
925 flesh_fields => {acqlid => ['fund']}
930 unless ($lid->fund) {
932 new OpenILS::Event("ACQ_FUND_NOT_FOUND") # close enough
937 my $amount = $li->estimated_unit_price;
938 if($li->provider->currency_type ne $lid->fund->currency_type and !$no_translate) {
940 # At Fund debit creation time, translate into the currency of the fund
941 # TODO: org setting to disable automatic currency conversion at debit create time?
943 $amount = $mgr->editor->json_query({
945 'acq.exchange_ratio',
946 $li->provider->currency_type, # source currency
947 $lid->fund->currency_type, # destination currency
948 $li->estimated_unit_price # source amount
950 })->[0]->{'acq.exchange_ratio'};
953 my $debit = create_fund_debit(
956 fund => $lid->fund->id,
957 origin_amount => $li->estimated_unit_price,
958 origin_currency_type => $li->provider->currency_type,
962 $lid->fund_debit($debit->id);
963 $lid->fund($lid->fund->id);
964 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
969 __PACKAGE__->register_method(
970 "method" => "fund_exceeds_balance_percent_api",
971 "api_name" => "open-ils.acq.fund.check_balance_percentages",
973 "desc" => q/Determine whether a given fund exceeds its defined
974 "balance stop and warning percentages"/,
976 {"desc" => "Authentication token", "type" => "string"},
977 {"desc" => "Fund ID", "type" => "number"},
978 {"desc" => "Theoretical debit amount (optional)",
981 "return" => {"desc" => q/An array of two values, for stop and warning,
982 in that order: 1 if fund exceeds that balance percentage, else 0/}
986 sub fund_exceeds_balance_percent_api {
987 my ($self, $conn, $auth, $fund_id, $debit_amount) = @_;
991 my $e = new_editor("authtoken" => $auth);
992 return $e->die_event unless $e->checkauth;
994 my $fund = $e->retrieve_acq_fund($fund_id) or return $e->die_event;
995 return $e->die_event unless $e->allowed("VIEW_FUND", $fund->org);
998 fund_exceeds_balance_percent($fund, $debit_amount, $e, "stop"),
999 fund_exceeds_balance_percent($fund, $debit_amount, $e, "warning")
1006 sub fund_exceeds_balance_percent {
1007 my ($fund, $debit_amount, $e, $which) = @_;
1009 my ($method_name, $event_name) = @{{
1011 "balance_warning_percent", "ACQ_FUND_EXCEEDS_WARN_PERCENT"
1014 "balance_stop_percent", "ACQ_FUND_EXCEEDS_STOP_PERCENT"
1018 if ($fund->$method_name) {
1020 $e->search_acq_fund_combined_balance({"fund" => $fund->id})->[0];
1022 $e->search_acq_fund_allocation_total({"fund" => $fund->id})->[0];
1024 $balance = ($balance) ? $balance->amount : 0;
1025 $allocations = ($allocations) ? $allocations->amount : 0;
1028 $allocations == 0 || # if no allocations were ever made, assume we have hit the stop percent
1029 ((($allocations - $balance + $debit_amount) / $allocations) * 100) > $fund->$method_name
1031 $logger->info("fund would hit a limit: " . $fund->id . ", $balance, $debit_amount, $allocations, $method_name");
1036 "fund" => $fund, "debit_amount" => $debit_amount
1046 # ----------------------------------------------------------------------------
1048 # ----------------------------------------------------------------------------
1049 sub create_fund_debit {
1050 my($mgr, $dry_run, %args) = @_;
1052 # Verify the fund is not being spent beyond the hard stop amount
1053 my $fund = $mgr->editor->retrieve_acq_fund($args{fund}) or return 0;
1056 fund_exceeds_balance_percent(
1057 $fund, $args{"amount"}, $mgr->editor, "stop"
1060 $dry_run and fund_exceeds_balance_percent(
1061 $fund, $args{"amount"}, $mgr->editor, "warning"
1064 my $debit = Fieldmapper::acq::fund_debit->new;
1065 $debit->debit_type('purchase');
1066 $debit->encumbrance('t');
1067 $debit->$_($args{$_}) for keys %args;
1069 $mgr->add_debit($debit->amount);
1070 return $mgr->editor->create_acq_fund_debit($debit);
1074 # ----------------------------------------------------------------------------
1076 # ----------------------------------------------------------------------------
1077 sub create_picklist {
1078 my($mgr, %args) = @_;
1079 my $picklist = Fieldmapper::acq::picklist->new;
1080 $picklist->creator($mgr->editor->requestor->id);
1081 $picklist->owner($picklist->creator);
1082 $picklist->editor($picklist->creator);
1083 $picklist->create_time('now');
1084 $picklist->edit_time('now');
1085 $picklist->org_unit($mgr->editor->requestor->ws_ou);
1086 $picklist->owner($mgr->editor->requestor->id);
1087 $picklist->$_($args{$_}) for keys %args;
1088 $picklist->clear_id;
1089 $mgr->picklist($picklist);
1090 return $mgr->editor->create_acq_picklist($picklist);
1093 sub update_picklist {
1094 my($mgr, $picklist) = @_;
1095 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1096 $picklist->edit_time('now');
1097 $picklist->editor($mgr->editor->requestor->id);
1098 if ($mgr->editor->update_acq_picklist($picklist)) {
1099 $picklist = $mgr->editor->retrieve_acq_picklist($mgr->editor->data);
1100 $mgr->picklist($picklist);
1107 sub delete_picklist {
1108 my($mgr, $picklist) = @_;
1109 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
1111 # delete all 'new' lineitems
1112 my $li_ids = $mgr->editor->search_acq_lineitem(
1114 picklist => $picklist->id,
1115 "-or" => {state => "new", purchase_order => undef}
1119 for my $li_id (@$li_ids) {
1120 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1121 return 0 unless delete_lineitem($mgr, $li);
1125 # detach all non-'new' lineitems
1126 $li_ids = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}}, {idlist => 1});
1127 for my $li_id (@$li_ids) {
1128 my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
1129 $li->clear_picklist;
1130 return 0 unless update_lineitem($mgr, $li);
1134 # remove any picklist-specific object perms
1135 my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
1136 for my $op (@$ops) {
1137 return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
1140 return $mgr->editor->delete_acq_picklist($picklist);
1143 # ----------------------------------------------------------------------------
1145 # ----------------------------------------------------------------------------
1146 sub update_purchase_order {
1148 $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
1149 $po->editor($mgr->editor->requestor->id);
1150 $po->edit_time('now');
1151 $mgr->purchase_order($po);
1152 return $mgr->editor->retrieve_acq_purchase_order($mgr->editor->data)
1153 if $mgr->editor->update_acq_purchase_order($po);
1157 sub create_purchase_order {
1158 my($mgr, %args) = @_;
1160 # verify the chosen provider is still active
1161 my $provider = $mgr->editor->retrieve_acq_provider($args{provider}) or return 0;
1162 unless($U->is_true($provider->active)) {
1163 $logger->error("provider is not active. cannot create PO");
1164 $mgr->editor->event(OpenILS::Event->new('ACQ_PROVIDER_INACTIVE'));
1168 my $po = Fieldmapper::acq::purchase_order->new;
1169 $po->creator($mgr->editor->requestor->id);
1170 $po->editor($mgr->editor->requestor->id);
1171 $po->owner($mgr->editor->requestor->id);
1172 $po->edit_time('now');
1173 $po->create_time('now');
1174 $po->state('pending');
1175 $po->ordering_agency($mgr->editor->requestor->ws_ou);
1176 $po->$_($args{$_}) for keys %args;
1178 $mgr->purchase_order($po);
1179 return $mgr->editor->create_acq_purchase_order($po);
1182 # ----------------------------------------------------------------------------
1183 # if all of the lineitems for this PO are received and no
1184 # blanket charges are still encumbered, mark the PO as received.
1185 # ----------------------------------------------------------------------------
1186 sub check_purchase_order_received {
1187 my($mgr, $po_id) = @_;
1189 my $non_recv_li = $mgr->editor->json_query({
1194 "jub" => {"acqcr" => {"type" => "left"}}
1197 "+jub" => {"id" => $po_id},
1199 {"+jub" => {"state" => "received"}},
1200 {"+acqcr" => {"keep_debits" =>"t"}}
1205 my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
1206 return $po if @$non_recv_li;
1208 # avoid marking the PO as received if any blanket charges
1209 # are still encumbered.
1210 my $blankets = $mgr->editor->json_query({
1211 select => {acqpoi => ['id']},
1214 aiit => {filter => {blanket=>'t'}},
1215 acqfdeb => {filter => {encumbrance => 't'}}
1218 where => {'+acqpoi' => {purchase_order => $po_id}}
1221 return $po if @$blankets;
1223 $po->state('received');
1224 return update_purchase_order($mgr, $po);
1228 # ----------------------------------------------------------------------------
1229 # Bib, Callnumber, and Copy data
1230 # ----------------------------------------------------------------------------
1232 sub create_lineitem_assets {
1233 my($mgr, $li_id) = @_;
1236 my $li = $mgr->editor->retrieve_acq_lineitem([
1239 flesh_fields => {jub => ['purchase_order', 'attributes']}
1243 # note: at this point, the bib record this LI links to should already be created
1245 # -----------------------------------------------------------------
1246 # The lineitem is going live, promote user request holds to real holds
1247 # -----------------------------------------------------------------
1248 promote_lineitem_holds($mgr, $li) or return 0;
1250 my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
1252 # -----------------------------------------------------------------
1253 # for each lineitem_detail, create the volume if necessary, create
1254 # a copy, and link them all together.
1255 # -----------------------------------------------------------------
1257 for my $lid_id (@{$li_details}) {
1259 my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
1260 next if $lid->eg_copy_id;
1262 # use the same callnumber label for all items within this lineitem
1263 $lid->cn_label($first_cn) if $first_cn and not $lid->cn_label;
1265 # apply defaults if necessary
1266 return 0 unless complete_lineitem_detail($mgr, $lid);
1268 $first_cn = $lid->cn_label unless $first_cn;
1270 my $org = $lid->owning_lib;
1271 my $label = $lid->cn_label;
1272 my $bibid = $li->eg_bib_id;
1274 my $volume = $mgr->cache($org, "cn.$bibid.$label");
1276 $volume = create_volume($mgr, $li, $lid) or return 0;
1277 $mgr->cache($org, "cn.$bibid.$label", $volume);
1279 create_copy($mgr, $volume, $lid, $li) or return 0;
1282 return { li => $li };
1286 my($mgr, $li, $lid) = @_;
1288 my ($volume, $evt) =
1289 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
1297 $mgr->editor->event($evt);
1305 my($mgr, $volume, $lid, $li) = @_;
1306 my $copy = Fieldmapper::asset::copy->new;
1308 $copy->loan_duration(2);
1309 $copy->fine_level(2);
1310 $copy->status(($lid->recv_time) ? OILS_COPY_STATUS_IN_PROCESS : OILS_COPY_STATUS_ON_ORDER);
1311 $copy->barcode($lid->barcode);
1312 $copy->location($lid->location);
1313 $copy->call_number($volume->id);
1314 $copy->circ_lib($volume->owning_lib);
1315 $copy->circ_modifier($lid->circ_modifier);
1317 # AKA list price. We might need a $li->list_price field since
1318 # estimated price is not necessarily the same as list price
1319 $copy->price($li->estimated_unit_price);
1321 my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
1323 $mgr->editor->event($evt);
1328 $lid->eg_copy_id($copy->id);
1329 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
1337 # ----------------------------------------------------------------------------
1338 # Workflow: Build a selection list from a Z39.50 search
1339 # ----------------------------------------------------------------------------
1341 __PACKAGE__->register_method(
1342 method => 'zsearch',
1343 api_name => 'open-ils.acq.picklist.search.z3950',
1346 desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
1348 {desc => 'Authentication token', type => 'string'},
1349 {desc => 'Search definition', type => 'object'},
1350 {desc => 'Picklist name, optional', type => 'string'},
1356 my($self, $conn, $auth, $search, $name, $options) = @_;
1357 my $e = new_editor(authtoken=>$auth);
1358 return $e->event unless $e->checkauth;
1359 return $e->event unless $e->allowed('CREATE_PICKLIST');
1361 $search->{limit} ||= 10;
1364 my $ses = OpenSRF::AppSession->create('open-ils.search');
1365 my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
1370 while(my $resp = $req->recv(timeout=>60)) {
1373 my $e = new_editor(requestor=>$e->requestor, xact=>1);
1374 $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1375 $picklist = zsearch_build_pl($mgr, $name);
1379 my $result = $resp->content;
1380 my $count = $result->{count} || 0;
1381 $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
1383 for my $rec (@{$result->{records}}) {
1385 my $li = create_lineitem($mgr,
1386 picklist => $picklist->id,
1387 source_label => $result->{service},
1388 marc => $rec->{marcxml},
1389 eg_bib_id => $rec->{bibid}
1392 if($$options{respond_li}) {
1393 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
1394 if $$options{flesh_attrs};
1395 $li->clear_marc if $$options{clear_marc};
1396 $mgr->respond(lineitem => $li);
1403 $mgr->editor->commit;
1404 return $mgr->respond_complete;
1407 sub zsearch_build_pl {
1408 my($mgr, $name) = @_;
1411 my $picklist = $mgr->editor->search_acq_picklist({
1412 owner => $mgr->editor->requestor->id,
1416 if($name eq '' and $picklist) {
1417 return 0 unless delete_picklist($mgr, $picklist);
1421 return update_picklist($mgr, $picklist) if $picklist;
1422 return create_picklist($mgr, name => $name);
1426 # ----------------------------------------------------------------------------
1427 # Workflow: Build a selection list / PO by importing a batch of MARC records
1428 # ----------------------------------------------------------------------------
1430 __PACKAGE__->register_method(
1431 method => 'upload_records',
1432 api_name => 'open-ils.acq.process_upload_records',
1434 max_chunk_count => 1
1437 sub upload_records {
1438 my($self, $conn, $auth, $key, $args) = @_;
1441 my $e = new_editor(authtoken => $auth, xact => 1);
1442 return $e->die_event unless $e->checkauth;
1443 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1445 my $cache = OpenSRF::Utils::Cache->new;
1447 my $data = $cache->get_cache("vandelay_import_spool_$key");
1448 my $filename = $data->{path};
1449 my $provider = $args->{provider};
1450 my $picklist = $args->{picklist};
1451 my $create_po = $args->{create_po};
1452 my $activate_po = $args->{activate_po};
1453 my $vandelay = $args->{vandelay};
1454 my $ordering_agency = $args->{ordering_agency} || $e->requestor->ws_ou;
1455 my $fiscal_year = $args->{fiscal_year};
1457 # if the user provides no fiscal year, find the
1458 # current fiscal year for the ordering agency.
1459 $fiscal_year ||= $U->simplereq(
1461 'open-ils.acq.org_unit.current_fiscal_year',
1469 unless(-r $filename) {
1470 $logger->error("unable to read MARC file $filename");
1472 return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
1475 $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
1478 $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
1479 if($picklist->owner != $e->requestor->id) {
1480 return $e->die_event unless
1481 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
1483 $mgr->picklist($picklist);
1487 return $e->die_event unless
1488 $e->allowed('CREATE_PURCHASE_ORDER', $ordering_agency);
1490 $po = create_purchase_order($mgr,
1491 ordering_agency => $ordering_agency,
1492 provider => $provider->id,
1493 state => 'pending' # will be updated later if activated
1494 ) or return $mgr->editor->die_event;
1497 $logger->info("acq processing MARC file=$filename");
1499 my $batch = new MARC::Batch ('USMARC', $filename);
1507 my ($err, $xml, $r);
1512 } catch Error with {
1514 $logger->warn("Proccessing of record $count in set $key failed with error $err. Skipping this record");
1521 $xml = clean_marc($r);
1522 } catch Error with {
1524 $logger->warn("Proccessing XML of record $count in set $key failed with error $err. Skipping this record");
1527 next if $err or not $xml;
1530 source_label => $provider->code,
1531 provider => $provider->id,
1535 $args{picklist} = $picklist->id if $picklist;
1537 $args{purchase_order} = $po->id;
1538 $args{state} = 'pending-order';
1541 my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
1543 $li->provider($provider); # flesh it, we'll need it later
1545 import_lineitem_details($mgr, $ordering_agency, $li, $fiscal_year)
1546 or return $mgr->editor->die_event;
1549 push(@li_list, $li->id);
1554 $evt = extract_po_name($mgr, $po, \@li_list);
1555 return $evt if $evt;
1560 $cache->delete_cache('vandelay_import_spool_' . $key);
1562 if ($po and $activate_po) {
1563 my $die_event = activate_purchase_order_impl($mgr, $po->id, $vandelay);
1564 return $die_event if $die_event;
1566 } elsif ($vandelay) {
1567 $vandelay->{new_rec_perm} = 'IMPORT_ACQ_LINEITEM_BIB_RECORD_UPLOAD';
1568 create_lineitem_list_assets($mgr, \@li_list, $vandelay,
1569 !$vandelay->{create_assets}) or return $e->die_event;
1572 return $mgr->respond_complete;
1575 # see if the PO name is encoded in the newly imported records
1576 sub extract_po_name {
1577 my ($mgr, $po, $li_ids) = @_;
1578 my $e = $mgr->editor;
1580 # find the first instance of the name
1581 my $attr = $e->search_acq_lineitem_attr([
1582 { lineitem => $li_ids,
1583 attr_type => 'lineitem_provider_attr_definition',
1584 attr_name => 'purchase_order'
1586 order_by => {aqlia => 'id'},
1589 ])->[0] or return undef;
1591 my $name = $attr->attr_value;
1593 # see if another PO already has the name, provider, and org
1594 my $existing = $e->search_acq_purchase_order(
1596 ordering_agency => $po->ordering_agency,
1597 provider => $po->provider
1602 # if a PO exists with the same name (and provider/org)
1603 # tack the po ID into the name to differentiate
1604 $name = sprintf("$name (%s)", $po->id) if $existing;
1606 $logger->info("Extracted PO name: $name");
1609 update_purchase_order($mgr, $po) or return $e->die_event;
1613 sub import_lineitem_details {
1614 my($mgr, $ordering_agency, $li, $fiscal_year) = @_;
1616 my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
1617 return 1 unless @$holdings;
1618 my $org_path = $U->get_org_ancestors($ordering_agency);
1619 $org_path = [ reverse (@$org_path) ];
1625 # create a lineitem detail for each copy in the data
1627 my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx, $fiscal_year);
1628 last unless defined $compiled;
1629 return 0 unless $compiled;
1631 # this takes the price of the last copy and uses it as the lineitem price
1632 # need to determine if a given record would include different prices for the same item
1633 $price = $$compiled{estimated_price};
1635 last unless $$compiled{quantity};
1637 for(1..$$compiled{quantity}) {
1638 my $lid = create_lineitem_detail(
1640 lineitem => $li->id,
1641 owning_lib => $$compiled{owning_lib},
1642 cn_label => $$compiled{call_number},
1643 fund => $$compiled{fund},
1644 circ_modifier => $$compiled{circ_modifier},
1645 note => $$compiled{note},
1646 location => $$compiled{copy_location},
1647 collection_code => $$compiled{collection_code},
1648 barcode => $$compiled{barcode}
1656 $li->estimated_unit_price($price);
1657 update_lineitem($mgr, $li) or return 0;
1661 # return hash on success, 0 on error, undef on no more holdings
1662 sub extract_lineitem_detail_data {
1663 my($mgr, $org_path, $holdings, $index, $fiscal_year) = @_;
1665 my @data_list = grep { $_->{holding} eq $index } @$holdings;
1666 return undef unless @data_list;
1668 my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1669 my $base_org = $$org_path[0];
1673 $logger->error("Item import extraction error: $msg");
1674 $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1675 $mgr->editor->rollback;
1676 $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1680 # ---------------------------------------------------------------------
1682 if(my $code = $compiled{fund_code}) {
1684 my $fund = $mgr->cache($base_org, "fund.$code");
1686 # search up the org tree for the most appropriate fund
1687 for my $org (@$org_path) {
1688 $fund = $mgr->editor->search_acq_fund(
1689 {org => $org, code => $code, year => $fiscal_year}, {idlist => 1})->[0];
1693 return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1694 $compiled{fund} = $fund;
1695 $mgr->cache($base_org, "fund.$code", $fund);
1699 # ---------------------------------------------------------------------
1701 if(my $sn = $compiled{owning_lib}) {
1702 my $org_id = $mgr->cache($base_org, "orgsn.$sn") ||
1703 $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1704 return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1705 $compiled{owning_lib} = $org_id;
1706 $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1710 # ---------------------------------------------------------------------
1712 my $code = $compiled{circ_modifier};
1716 # verify this is a valid circ modifier
1717 return $killme->("invlalid circ_modifier $code") unless
1718 defined $mgr->cache($base_org, "mod.$code") or
1719 $mgr->editor->retrieve_config_circ_modifier($code);
1721 # if valid, cache for future tests
1722 $mgr->cache($base_org, "mod.$code", $code);
1725 $compiled{circ_modifier} = get_default_circ_modifier($mgr, $base_org);
1729 # ---------------------------------------------------------------------
1731 if( my $name = $compiled{copy_location}) {
1733 my $cp_base_org = $base_org;
1735 if ($compiled{owning_lib}) {
1736 # start looking for copy locations at the copy
1737 # owning lib instaed of the upload context org
1738 $cp_base_org = $compiled{owning_lib};
1741 my $loc = $mgr->cache($cp_base_org, "copy_loc.$name");
1743 my $org = $cp_base_org;
1745 $loc = $mgr->editor->search_asset_copy_location(
1746 {owning_lib => $org, name => $name, deleted => 'f'}, {idlist => 1})->[0];
1748 $org = $mgr->editor->retrieve_actor_org_unit($org)->parent_ou;
1751 return $killme->("Invalid copy location $name") unless $loc;
1752 $compiled{copy_location} = $loc;
1753 $mgr->cache($cp_base_org, "copy_loc.$name", $loc);
1761 # ----------------------------------------------------------------------------
1762 # Workflow: Given an existing purchase order, import/create the bibs,
1763 # callnumber and copy objects
1764 # ----------------------------------------------------------------------------
1766 __PACKAGE__->register_method(
1767 method => 'create_po_assets',
1768 api_name => 'open-ils.acq.purchase_order.assets.create',
1770 desc => q/Creates assets for each lineitem in the purchase order/,
1772 {desc => 'Authentication token', type => 'string'},
1773 {desc => 'The purchase order id', type => 'number'},
1775 return => {desc => 'Streams a total versus completed counts object, event on error'}
1777 max_chunk_count => 1
1780 sub create_po_assets {
1781 my($self, $conn, $auth, $po_id, $args) = @_;
1784 my $e = new_editor(authtoken=>$auth, xact=>1);
1785 return $e->die_event unless $e->checkauth;
1786 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1788 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1790 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1792 # it's ugly, but it's fast. Get the total count of lineitem detail objects to process
1793 my $lid_total = $e->json_query({
1794 select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] },
1800 join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1804 where => {'+acqpo' => {id => $po_id}}
1807 # maximum number of Vandelay bib actions is twice
1808 # the number line items (queue bib, then create it)
1809 $mgr->total(scalar(@$li_ids) * 2 + $lid_total);
1811 create_lineitem_list_assets($mgr, $li_ids, $args->{vandelay})
1812 or return $e->die_event;
1815 update_purchase_order($mgr, $po) or return $e->die_event;
1818 return $mgr->respond_complete;
1823 __PACKAGE__->register_method(
1824 method => 'create_purchase_order_api',
1825 api_name => 'open-ils.acq.purchase_order.create',
1827 desc => 'Creates a new purchase order',
1829 {desc => 'Authentication token', type => 'string'},
1830 {desc => 'purchase_order to create', type => 'object'}
1832 return => {desc => 'The purchase order id, Event on failure'}
1834 max_chunk_count => 1
1837 sub create_purchase_order_api {
1838 my($self, $conn, $auth, $po, $args) = @_;
1841 my $e = new_editor(xact=>1, authtoken=>$auth);
1842 return $e->die_event unless $e->checkauth;
1843 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1844 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1847 my %pargs = (ordering_agency => $e->requestor->ws_ou); # default
1848 $pargs{provider} = $po->provider if $po->provider;
1849 $pargs{ordering_agency} = $po->ordering_agency if $po->ordering_agency;
1850 $pargs{prepayment_required} = $po->prepayment_required if $po->prepayment_required;
1851 $pargs{name} = $po->name if $po->name;
1852 my $vandelay = $args->{vandelay};
1854 $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1856 my $li_ids = $$args{lineitems};
1860 for my $li_id (@$li_ids) {
1862 my $li = $e->retrieve_acq_lineitem([
1864 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1865 ]) or return $e->die_event;
1867 return $e->die_event(
1869 "BAD_PARAMS", payload => $li,
1870 note => "acq.lineitem #" . $li->id .
1871 ": purchase_order #" . $li->purchase_order
1873 ) if $li->purchase_order;
1875 $li->provider($po->provider);
1876 $li->purchase_order($po->id);
1877 $li->state('pending-order');
1878 update_lineitem($mgr, $li) or return $e->die_event;
1883 # see if we have a PO name encoded in any of our lineitems
1884 my $evt = extract_po_name($mgr, $po, $li_ids);
1885 return $evt if $evt;
1887 # commit before starting the asset creation
1893 create_lineitem_list_assets(
1894 $mgr, $li_ids, $vandelay, !$$args{create_assets})
1895 or return $e->die_event;
1899 apply_default_copies($mgr, $po) or return $e->die_event;
1903 return $mgr->respond_complete;
1906 # !transaction must be managed by the caller
1907 # creates the default number of copies for each lineitem on the PO.
1908 # when a LI already has copies attached, no default copies are added.
1909 # without li_id, all lineitems are checked/applied
1910 # returns 1 on success, 0 on error
1911 sub apply_default_copies {
1912 my ($mgr, $po, $li_id) = @_;
1914 my $e = $mgr->editor;
1916 my $provider = ref($po->provider) ? $po->provider :
1917 $e->retrieve_acq_provider($po->provider);
1919 my $copy_count = $provider->default_copy_count || return 1;
1921 $logger->info("Applying $copy_count default copies for PO ".$po->id);
1923 my $li_ids = $li_id ? [$li_id] :
1924 $e->search_acq_lineitem({
1925 purchase_order => $po->id,
1926 cancel_reason => undef
1931 for my $li_id (@$li_ids) {
1933 my $lid_ids = $e->search_acq_lineitem_detail(
1934 {lineitem => $li_id}, {idlist => 1});
1936 # do not apply default copies when copies already exist
1939 for (1 .. $copy_count) {
1940 create_lineitem_detail($mgr,
1942 owning_lib => $e->requestor->ws_ou
1952 __PACKAGE__->register_method(
1953 method => 'update_lineitem_fund_batch',
1954 api_name => 'open-ils.acq.lineitem.fund.update.batch',
1957 desc => q/Given a set of lineitem IDS, updates the fund for all attached lineitem details/
1961 sub update_lineitem_fund_batch {
1962 my($self, $conn, $auth, $li_ids, $fund_id) = @_;
1963 my $e = new_editor(xact=>1, authtoken=>$auth);
1964 return $e->die_event unless $e->checkauth;
1965 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1966 for my $li_id (@$li_ids) {
1967 my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
1968 return $evt if $evt;
1969 my $li_details = $e->search_acq_lineitem_detail({lineitem => $li_id});
1970 $_->fund($fund_id) and $_->ischanged(1) for @$li_details;
1971 $evt = lineitem_detail_CUD_batch($mgr, $li_details);
1972 return $evt if $evt;
1977 return $mgr->respond_complete;
1982 __PACKAGE__->register_method(
1983 method => 'lineitem_detail_CUD_batch_api',
1984 api_name => 'open-ils.acq.lineitem_detail.cud.batch',
1987 desc => q/Creates a new purchase order line item detail. / .
1988 q/Additionally creates the associated fund_debit/,
1990 {desc => 'Authentication token', type => 'string'},
1991 {desc => 'List of lineitem_details to create', type => 'array'},
1992 {desc => 'Create Debits. Used for creating post-po-asset-creation debits', type => 'bool'},
1994 return => {desc => 'Streaming response of current position in the array'}
1998 __PACKAGE__->register_method(
1999 method => 'lineitem_detail_CUD_batch_api',
2000 api_name => 'open-ils.acq.lineitem_detail.cud.batch.dry_run',
2004 Dry run version of open-ils.acq.lineitem_detail.cud.batch.
2005 In dry_run mode, updated fund_debit's the exceed the warning
2006 percent return an event.
2012 sub lineitem_detail_CUD_batch_api {
2013 my($self, $conn, $auth, $li_details, $create_debits) = @_;
2014 my $e = new_editor(xact=>1, authtoken=>$auth);
2015 return $e->die_event unless $e->checkauth;
2016 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2017 my $dry_run = ($self->api_name =~ /dry_run/o);
2018 my $evt = lineitem_detail_CUD_batch($mgr, $li_details, $create_debits, $dry_run);
2019 return $evt if $evt;
2021 return $mgr->respond_complete;
2025 sub lineitem_detail_CUD_batch {
2026 my($mgr, $li_details, $create_debits, $dry_run) = @_;
2028 $mgr->total(scalar(@$li_details));
2029 my $e = $mgr->editor;
2033 my $fund_cache = {};
2036 for my $lid (@$li_details) {
2038 unless($li = $li_cache{$lid->lineitem}) {
2039 ($li, $evt) = fetch_and_check_li($e, $lid->lineitem, 'write');
2040 return $evt if $evt;
2044 $lid = create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
2045 if($create_debits) {
2046 $li->provider($e->retrieve_acq_provider($li->provider)) or return $e->die_event;
2047 $lid->fund($e->retrieve_acq_fund($lid->fund)) or return $e->die_event;
2048 create_lineitem_detail_debit($mgr, $li, $lid, 0, 1) or return $e->die_event;
2051 } elsif($lid->ischanged) {
2052 return $evt if $evt = handle_changed_lid($e, $lid, $dry_run, $fund_cache);
2054 } elsif($lid->isdeleted) {
2055 delete_lineitem_detail($mgr, $lid) or return $e->die_event;
2058 $mgr->respond(li => $li);
2059 $li_cache{$lid->lineitem} = $li;
2065 sub handle_changed_lid {
2066 my($e, $lid, $dry_run, $fund_cache) = @_;
2068 my $orig_lid = $e->retrieve_acq_lineitem_detail($lid->id) or return $e->die_event;
2070 # updating the fund, so update the debit
2071 if($orig_lid->fund_debit and $orig_lid->fund != $lid->fund) {
2073 my $debit = $e->retrieve_acq_fund_debit($orig_lid->fund_debit);
2074 my $new_fund = $$fund_cache{$lid->fund} =
2075 $$fund_cache{$lid->fund} || $e->retrieve_acq_fund($lid->fund);
2077 # check the thresholds
2078 return $e->die_event if
2079 fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "stop");
2080 return $e->die_event if $dry_run and
2081 fund_exceeds_balance_percent($new_fund, $debit->amount, $e, "warning");
2083 $debit->fund($new_fund->id);
2084 $e->update_acq_fund_debit($debit) or return $e->die_event;
2087 $e->update_acq_lineitem_detail($lid) or return $e->die_event;
2092 __PACKAGE__->register_method(
2093 method => 'receive_po_api',
2094 api_name => 'open-ils.acq.purchase_order.receive'
2097 sub receive_po_api {
2098 my($self, $conn, $auth, $po_id) = @_;
2099 my $e = new_editor(xact => 1, authtoken => $auth);
2100 return $e->die_event unless $e->checkauth;
2101 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2103 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2104 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2106 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2108 for my $li_id (@$li_ids) {
2109 receive_lineitem($mgr, $li_id) or return $e->die_event;
2113 $po->state('received');
2114 update_purchase_order($mgr, $po) or return $e->die_event;
2117 return $mgr->respond_complete;
2121 # At the moment there's a lack of parallelism between the receive and unreceive
2122 # API methods for POs and the API methods for LIs and LIDs. The methods for
2123 # POs stream back objects as they act, whereas the methods for LIs and LIDs
2124 # atomically return an object that describes only what changed (in LIs and LIDs
2125 # themselves or in the objects to which to LIs and LIDs belong).
2127 # The methods for LIs and LIDs work the way they do to faciliate the UI's
2128 # maintaining correct information about the state of these things when a user
2129 # wants to receive or unreceive these objects without refreshing their whole
2130 # display. The UI feature for receiving and un-receiving a whole PO just
2131 # refreshes the whole display, so this absence of parallelism in the UI is also
2132 # relected in this module.
2134 # This could be neatened in the future by making POs receive and unreceive in
2135 # the same way the LIs and LIDs do.
2137 __PACKAGE__->register_method(
2138 method => 'receive_lineitem_detail_api',
2139 api_name => 'open-ils.acq.lineitem_detail.receive',
2141 desc => 'Mark a lineitem_detail as received',
2143 {desc => 'Authentication token', type => 'string'},
2144 {desc => 'lineitem detail ID', type => 'number'}
2147 "on success, object describing changes to LID and possibly " .
2148 "to LI and PO; on error, Event"
2153 sub receive_lineitem_detail_api {
2154 my($self, $conn, $auth, $lid_id) = @_;
2156 my $e = new_editor(xact=>1, authtoken=>$auth);
2157 return $e->die_event unless $e->checkauth;
2158 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2161 "flesh" => 2, "flesh_fields" => {
2162 "acqlid" => ["lineitem"], "jub" => ["purchase_order"]
2166 my $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2168 return $e->die_event unless $e->allowed(
2169 'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
2172 my $recvd = receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
2174 # .. and re-retrieve
2175 $lid = $e->retrieve_acq_lineitem_detail([$lid_id, $fleshing]);
2177 # Now build result data structure.
2178 my $result = {"lid" => {$lid->id => {"recv_time" => $lid->recv_time}}};
2181 if ($recvd->class_name =~ /::purchase_order/) {
2182 $result->{"po"} = describe_affected_po($e, $recvd);
2184 $lid->lineitem->id => {"state" => $lid->lineitem->state}
2186 } elsif ($recvd->class_name =~ /::lineitem/) {
2187 $result->{"li"} = {$recvd->id => {"state" => $recvd->state}};
2191 describe_affected_po($e, $lid->lineitem->purchase_order);
2197 __PACKAGE__->register_method(
2198 method => 'receive_lineitem_api',
2199 api_name => 'open-ils.acq.lineitem.receive',
2201 desc => 'Mark a lineitem as received',
2203 {desc => 'Authentication token', type => 'string'},
2204 {desc => 'lineitem ID', type => 'number'}
2207 "on success, object describing changes to LI and possibly PO; " .
2213 sub receive_lineitem_api {
2214 my($self, $conn, $auth, $li_id) = @_;
2216 my $e = new_editor(xact=>1, authtoken=>$auth);
2217 return $e->die_event unless $e->checkauth;
2218 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2220 my $li = $e->retrieve_acq_lineitem([
2224 jub => ['purchase_order']
2227 ]) or return $e->die_event;
2229 return $e->die_event unless $e->allowed(
2230 'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
2232 my $res = receive_lineitem($mgr, $li_id) or return $e->die_event;
2234 $conn->respond_complete($res);
2235 $mgr->run_post_response_hooks
2239 __PACKAGE__->register_method(
2240 method => 'receive_lineitem_batch_api',
2241 api_name => 'open-ils.acq.lineitem.receive.batch',
2244 desc => 'Mark lineitems as received',
2246 {desc => 'Authentication token', type => 'string'},
2247 {desc => 'lineitem ID list', type => 'array'}
2250 q/On success, stream of objects describing changes to LIs and
2251 possibly PO; onerror, Event. Any event, even after lots of other
2252 objects, should mean general failure of whole batch operation./
2257 sub receive_lineitem_batch_api {
2258 my ($self, $conn, $auth, $li_idlist) = @_;
2260 return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2262 my $e = new_editor(xact => 1, authtoken => $auth);
2263 return $e->die_event unless $e->checkauth;
2265 my $mgr = new OpenILS::Application::Acq::BatchManager(
2266 editor => $e, conn => $conn
2269 for my $li_id (map { int $_ } @$li_idlist) {
2270 my $li = $e->retrieve_acq_lineitem([
2273 flesh_fields => { jub => ['purchase_order'] }
2275 ]) or return $e->die_event;
2277 return $e->die_event unless $e->allowed(
2278 'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency
2281 receive_lineitem($mgr, $li_id) or return $e->die_event;
2285 $e->commit or return $e->die_event;
2286 $mgr->respond_complete;
2287 $mgr->run_post_response_hooks;
2290 __PACKAGE__->register_method(
2291 method => 'rollback_receive_po_api',
2292 api_name => 'open-ils.acq.purchase_order.receive.rollback'
2295 sub rollback_receive_po_api {
2296 my($self, $conn, $auth, $po_id) = @_;
2297 my $e = new_editor(xact => 1, authtoken => $auth);
2298 return $e->die_event unless $e->checkauth;
2299 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2301 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2302 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2304 my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
2306 for my $li_id (@$li_ids) {
2307 rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2311 $po->state('on-order');
2312 update_purchase_order($mgr, $po) or return $e->die_event;
2315 return $mgr->respond_complete;
2319 __PACKAGE__->register_method(
2320 method => 'rollback_receive_lineitem_detail_api',
2321 api_name => 'open-ils.acq.lineitem_detail.receive.rollback',
2323 desc => 'Mark a lineitem_detail as Un-received',
2325 {desc => 'Authentication token', type => 'string'},
2326 {desc => 'lineitem detail ID', type => 'number'}
2329 "on success, object describing changes to LID and possibly " .
2330 "to LI and PO; on error, Event"
2335 sub rollback_receive_lineitem_detail_api {
2336 my($self, $conn, $auth, $lid_id) = @_;
2338 my $e = new_editor(xact=>1, authtoken=>$auth);
2339 return $e->die_event unless $e->checkauth;
2340 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2342 my $lid = $e->retrieve_acq_lineitem_detail([
2346 acqlid => ['lineitem'],
2347 jub => ['purchase_order']
2351 my $li = $lid->lineitem;
2352 my $po = $li->purchase_order;
2354 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2358 my $recvd = rollback_receive_lineitem_detail($mgr, $lid_id)
2359 or return $e->die_event;
2362 $result->{"lid"} = {$recvd->id => {"recv_time" => $recvd->recv_time}};
2364 $result->{"lid"} = {$lid->id => {"recv_time" => $lid->recv_time}};
2367 if ($li->state eq "received") {
2368 $li->state("on-order");
2369 $li = update_lineitem($mgr, $li) or return $e->die_event;
2370 $result->{"li"} = {$li->id => {"state" => $li->state}};
2373 if ($po->state eq "received") {
2374 $po->state("on-order");
2375 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2377 $result->{"po"} = describe_affected_po($e, $po);
2379 $e->commit and return $result or return $e->die_event;
2382 __PACKAGE__->register_method(
2383 method => 'rollback_receive_lineitem_api',
2384 api_name => 'open-ils.acq.lineitem.receive.rollback',
2386 desc => 'Mark a lineitem as Un-received',
2388 {desc => 'Authentication token', type => 'string'},
2389 {desc => 'lineitem ID', type => 'number'}
2392 "on success, object describing changes to LI and possibly PO; " .
2398 sub rollback_receive_lineitem_api {
2399 my($self, $conn, $auth, $li_id) = @_;
2401 my $e = new_editor(xact=>1, authtoken=>$auth);
2402 return $e->die_event unless $e->checkauth;
2403 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2405 my $li = $e->retrieve_acq_lineitem([
2407 "flesh" => 1, "flesh_fields" => {"jub" => ["purchase_order"]}
2410 my $po = $li->purchase_order;
2412 return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2414 $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2416 my $result = {"li" => {$li->id => {"state" => $li->state}}};
2417 if ($po->state eq "received") {
2418 $po->state("on-order");
2419 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2421 $result->{"po"} = describe_affected_po($e, $po);
2423 $e->commit and return $result or return $e->die_event;
2426 __PACKAGE__->register_method(
2427 method => 'rollback_receive_lineitem_batch_api',
2428 api_name => 'open-ils.acq.lineitem.receive.rollback.batch',
2431 desc => 'Mark a list of lineitems as Un-received',
2433 {desc => 'Authentication token', type => 'string'},
2434 {desc => 'lineitem ID list', type => 'array'}
2437 q/on success, a stream of objects describing changes to LI and
2438 possibly PO; on error, Event. Any event means all previously
2439 returned objects indicate changes that didn't really happen./
2444 sub rollback_receive_lineitem_batch_api {
2445 my ($self, $conn, $auth, $li_idlist) = @_;
2447 return unless ref $li_idlist eq 'ARRAY' and @$li_idlist;
2449 my $e = new_editor(xact => 1, authtoken => $auth);
2450 return $e->die_event unless $e->checkauth;
2452 my $mgr = new OpenILS::Application::Acq::BatchManager(
2453 editor => $e, conn => $conn
2456 for my $li_id (map { int $_ } @$li_idlist) {
2457 my $li = $e->retrieve_acq_lineitem([
2460 "flesh_fields" => {"jub" => ["purchase_order"]}
2464 my $po = $li->purchase_order;
2466 return $e->die_event unless
2467 $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
2469 $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
2471 my $result = {"li" => {$li->id => {"state" => $li->state}}};
2472 if ($po->state eq "received") { # should happen first time, not after
2473 $po->state("on-order");
2474 $po = update_purchase_order($mgr, $po) or return $e->die_event;
2476 $result->{"po"} = describe_affected_po($e, $po);
2478 $mgr->respond(%$result);
2481 $e->commit or return $e->die_event;
2482 $mgr->respond_complete;
2483 $mgr->run_post_response_hooks;
2487 __PACKAGE__->register_method(
2488 method => 'set_lineitem_price_api',
2489 api_name => 'open-ils.acq.lineitem.price.set',
2491 desc => 'Set lineitem price. If debits already exist, update them as well',
2493 {desc => 'Authentication token', type => 'string'},
2494 {desc => 'lineitem ID', type => 'number'}
2496 return => {desc => 'status blob, Event on error'}
2500 sub set_lineitem_price_api {
2501 my($self, $conn, $auth, $li_id, $price) = @_;
2503 my $e = new_editor(xact=>1, authtoken=>$auth);
2504 return $e->die_event unless $e->checkauth;
2505 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2507 my ($li, $evt) = fetch_and_check_li($e, $li_id, 'write');
2508 return $evt if $evt;
2510 $li->estimated_unit_price($price);
2511 update_lineitem($mgr, $li) or return $e->die_event;
2513 my $lid_ids = $e->search_acq_lineitem_detail(
2514 {lineitem => $li_id, fund_debit => {'!=' => undef}},
2518 for my $lid_id (@$lid_ids) {
2520 my $lid = $e->retrieve_acq_lineitem_detail([
2522 flesh => 1, flesh_fields => {acqlid => ['fund', 'fund_debit']}}
2525 $lid->fund_debit->amount($price);
2526 $e->update_acq_fund_debit($lid->fund_debit) or return $e->die_event;
2532 return $mgr->respond_complete;
2536 __PACKAGE__->register_method(
2537 method => 'clone_picklist_api',
2538 api_name => 'open-ils.acq.picklist.clone',
2540 desc => 'Clones a picklist, including lineitem and lineitem details',
2542 {desc => 'Authentication token', type => 'string'},
2543 {desc => 'Picklist ID', type => 'number'},
2544 {desc => 'New Picklist Name', type => 'string'}
2546 return => {desc => 'status blob, Event on error'}
2550 sub clone_picklist_api {
2551 my($self, $conn, $auth, $pl_id, $name) = @_;
2553 my $e = new_editor(xact=>1, authtoken=>$auth);
2554 return $e->die_event unless $e->checkauth;
2555 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2557 my $old_pl = $e->retrieve_acq_picklist($pl_id);
2558 my $new_pl = create_picklist($mgr, %{$old_pl->to_bare_hash}, name => $name) or return $e->die_event;
2560 my $li_ids = $e->search_acq_lineitem({picklist => $pl_id}, {idlist => 1});
2562 # get the current user
2563 my $cloner = $mgr->editor->requestor->id;
2565 for my $li_id (@$li_ids) {
2567 # copy the lineitems' MARC
2568 my $marc = ($e->retrieve_acq_lineitem($li_id))->marc;
2570 # create a skeletal clone of the item
2571 my $li = Fieldmapper::acq::lineitem->new;
2572 $li->creator($cloner);
2573 $li->selector($cloner);
2574 $li->editor($cloner);
2577 my $new_li = create_lineitem($mgr, %{$li->to_bare_hash}, picklist => $new_pl->id) or return $e->die_event;
2583 return $mgr->respond_complete;
2587 __PACKAGE__->register_method(
2588 method => 'merge_picklist_api',
2589 api_name => 'open-ils.acq.picklist.merge',
2591 desc => 'Merges 2 or more picklists into a single list',
2593 {desc => 'Authentication token', type => 'string'},
2594 {desc => 'Lead Picklist ID', type => 'number'},
2595 {desc => 'List of subordinate picklist IDs', type => 'array'}
2597 return => {desc => 'status blob, Event on error'}
2601 sub merge_picklist_api {
2602 my($self, $conn, $auth, $lead_pl, $pl_list) = @_;
2604 my $e = new_editor(xact=>1, authtoken=>$auth);
2605 return $e->die_event unless $e->checkauth;
2606 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2608 # XXX perms on each picklist modified
2610 $lead_pl = $e->retrieve_acq_picklist($lead_pl) or return $e->die_event;
2611 # point all of the lineitems at the lead picklist
2612 my $li_ids = $e->search_acq_lineitem({picklist => $pl_list}, {idlist => 1});
2614 for my $li_id (@$li_ids) {
2615 my $li = $e->retrieve_acq_lineitem($li_id);
2616 $li->picklist($lead_pl);
2617 update_lineitem($mgr, $li) or return $e->die_event;
2621 # now delete the subordinate lists
2622 for my $pl_id (@$pl_list) {
2623 my $pl = $e->retrieve_acq_picklist($pl_id);
2624 $e->delete_acq_picklist($pl) or return $e->die_event;
2627 update_picklist($mgr, $lead_pl) or return $e->die_event;
2630 return $mgr->respond_complete;
2634 __PACKAGE__->register_method(
2635 method => 'delete_picklist_api',
2636 api_name => 'open-ils.acq.picklist.delete',
2638 desc => q/Deletes a picklist. It also deletes any lineitems in the "new" state. / .
2639 q/Other attached lineitems are detached/,
2641 {desc => 'Authentication token', type => 'string'},
2642 {desc => 'Picklist ID to delete', type => 'number'}
2644 return => {desc => '1 on success, Event on error'}
2648 sub delete_picklist_api {
2649 my($self, $conn, $auth, $picklist_id) = @_;
2650 my $e = new_editor(xact=>1, authtoken=>$auth);
2651 return $e->die_event unless $e->checkauth;
2652 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2653 my $pl = $e->retrieve_acq_picklist($picklist_id) or return $e->die_event;
2654 delete_picklist($mgr, $pl) or return $e->die_event;
2656 return $mgr->respond_complete;
2661 __PACKAGE__->register_method(
2662 method => 'activate_purchase_order',
2663 api_name => 'open-ils.acq.purchase_order.activate.dry_run'
2666 __PACKAGE__->register_method(
2667 method => 'activate_purchase_order',
2668 api_name => 'open-ils.acq.purchase_order.activate',
2670 desc => q/Activates a purchase order. This updates the status of the PO / .
2671 q/and Lineitems to 'on-order'. Activated PO's are ready for EDI delivery if appropriate./,
2673 {desc => 'Authentication token', type => 'string'},
2674 {desc => 'Purchase ID', type => 'number'}
2676 return => {desc => '1 on success, Event on error'}
2680 sub activate_purchase_order {
2681 my($self, $conn, $auth, $po_id, $vandelay, $options) = @_;
2683 $$options{dry_run} = ($self->api_name =~ /\.dry_run/) ? 1 : 0;
2685 my $e = new_editor(authtoken=>$auth);
2686 return $e->die_event unless $e->checkauth;
2687 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
2688 my $die_event = activate_purchase_order_impl($mgr, $po_id, $vandelay, $options);
2689 return $e->die_event if $die_event;
2690 $conn->respond_complete(1);
2691 $mgr->run_post_response_hooks unless $$options{dry_run};
2695 # xacts managed within
2696 sub activate_purchase_order_impl {
2697 my ($mgr, $po_id, $vandelay, $options) = @_;
2699 my $dry_run = $$options{dry_run};
2700 my $no_assets = $$options{no_assets};
2702 # read-only until lineitem asset creation
2703 my $e = $mgr->editor;
2706 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
2707 return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
2709 return $e->die_event(OpenILS::Event->new('PO_ALREADY_ACTIVATED'))
2710 if $po->order_date; # PO cannot be re-activated
2712 my $provider = $e->retrieve_acq_provider($po->provider);
2714 # find lineitems and create assets for all
2717 purchase_order => $po_id,
2718 state => [qw/pending-order new order-ready/]
2721 my $li_ids = $e->search_acq_lineitem($query, {idlist => 1});
2723 my $vl_resp; # imported li's and the managing queue
2724 unless ($dry_run or $no_assets) {
2725 $e->rollback; # read-only thus far
2727 # list_assets manages its own transactions
2728 $vl_resp = create_lineitem_list_assets($mgr, $li_ids, $vandelay)
2729 or return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED');
2733 # create fund debits for lineitems
2735 for my $li_id (@$li_ids) {
2736 my $li = $e->retrieve_acq_lineitem($li_id);
2738 unless ($li->eg_bib_id or $dry_run or $no_assets) {
2739 # we encountered a lineitem that was not successfully imported.
2740 # we cannot continue. rollback and report.
2742 return OpenILS::Event->new('ACQ_LI_IMPORT_FAILED', {queue => $vl_resp->{queue}});
2745 $li->state('on-order');
2746 $li->claim_policy($provider->default_claim_policy)
2747 if $provider->default_claim_policy and !$li->claim_policy;
2748 create_lineitem_debits($mgr, $li, $options) or return $e->die_event;
2749 update_lineitem($mgr, $li) or return $e->die_event;
2750 $mgr->post_process( sub { create_lineitem_status_events($mgr, $li->id, 'aur.ordered'); });
2754 # create po-item debits
2756 for my $po_item (@{$e->search_acq_po_item({purchase_order => $po_id})}) {
2758 my $debit = create_fund_debit(
2761 debit_type => 'direct_charge', # to match invoicing
2762 origin_amount => $po_item->estimated_cost,
2763 origin_currency_type => $e->retrieve_acq_fund($po_item->fund)->currency_type,
2764 amount => $po_item->estimated_cost,
2765 fund => $po_item->fund
2766 ) or return $e->die_event;
2767 $po_item->fund_debit($debit->id);
2768 $e->update_acq_po_item($po_item) or return $e->die_event;
2772 # mark PO as ordered
2774 $po->state('on-order');
2775 $po->order_date('now');
2776 update_purchase_order($mgr, $po) or return $e->die_event;
2779 $dry_run and $e->rollback or $e->commit;
2781 # tell the world we activated a PO
2782 $U->create_events_for_hook('acqpo.activated', $po, $po->ordering_agency) unless $dry_run;
2788 __PACKAGE__->register_method(
2789 method => 'split_purchase_order_by_lineitems',
2790 api_name => 'open-ils.acq.purchase_order.split_by_lineitems',
2792 desc => q/Splits a PO into many POs, 1 per lineitem. Only works for / .
2793 q/POs a) with more than one lineitems, and b) in the "pending" state./,
2795 {desc => 'Authentication token', type => 'string'},
2796 {desc => 'Purchase order ID', type => 'number'}
2798 return => {desc => 'list of new PO IDs on success, Event on error'}
2802 sub split_purchase_order_by_lineitems {
2803 my ($self, $conn, $auth, $po_id) = @_;
2805 my $e = new_editor("xact" => 1, "authtoken" => $auth);
2806 return $e->die_event unless $e->checkauth;
2808 my $po = $e->retrieve_acq_purchase_order([
2811 "flesh_fields" => {"acqpo" => [qw/lineitems notes/]}
2813 ]) or return $e->die_event;
2815 return $e->die_event
2816 unless $e->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2818 unless ($po->state eq "pending") {
2820 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_LATE");
2823 unless (@{$po->lineitems} > 1) {
2825 return new OpenILS::Event("ACQ_PURCHASE_ORDER_TOO_SHORT");
2828 # To split an existing PO into many, it seems unwise to just delete the
2829 # original PO, so we'll instead detach all of the original POs' lineitems
2830 # but the first, then create new POs for each of the remaining LIs, and
2831 # then attach the LIs to their new POs.
2833 my @po_ids = ($po->id);
2834 my @moving_li = @{$po->lineitems};
2835 shift @moving_li; # discard first LI
2837 foreach my $li (@moving_li) {
2838 my $new_po = $po->clone;
2840 $new_po->clear_name;
2841 $new_po->creator($e->requestor->id);
2842 $new_po->editor($e->requestor->id);
2843 $new_po->owner($e->requestor->id);
2844 $new_po->edit_time("now");
2845 $new_po->create_time("now");
2847 $new_po = $e->create_acq_purchase_order($new_po);
2849 # Clone any notes attached to the old PO and attach to the new one.
2850 foreach my $note (@{$po->notes}) {
2851 my $new_note = $note->clone;
2852 $new_note->clear_id;
2853 $new_note->edit_time("now");
2854 $new_note->purchase_order($new_po->id);
2855 $e->create_acq_po_note($new_note);
2858 $li->edit_time("now");
2859 $li->purchase_order($new_po->id);
2860 $e->update_acq_lineitem($li);
2862 push @po_ids, $new_po->id;
2865 $po->edit_time("now");
2866 $e->update_acq_purchase_order($po);
2868 return \@po_ids if $e->commit;
2869 return $e->die_event;
2873 sub not_cancelable {
2875 (ref $o eq "HASH" and $o->{"textcode"} eq "ACQ_NOT_CANCELABLE");
2878 __PACKAGE__->register_method(
2879 method => "cancel_purchase_order_api",
2880 api_name => "open-ils.acq.purchase_order.cancel",
2882 desc => q/Cancels an on-order purchase order/,
2884 {desc => "Authentication token", type => "string"},
2885 {desc => "PO ID to cancel", type => "number"},
2886 {desc => "Cancel reason ID", type => "number"}
2888 return => {desc => q/Object describing changed POs, LIs and LIDs
2889 on success; Event on error./}
2893 sub cancel_purchase_order_api {
2894 my ($self, $conn, $auth, $po_id, $cancel_reason) = @_;
2896 my $e = new_editor("xact" => 1, "authtoken" => $auth);
2897 return $e->die_event unless $e->checkauth;
2898 my $mgr = new OpenILS::Application::Acq::BatchManager(
2899 "editor" => $e, "conn" => $conn
2902 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
2903 return new OpenILS::Event(
2904 "BAD_PARAMS", "note" => "Provide cancel reason ID"
2907 my $result = cancel_purchase_order($mgr, $po_id, $cancel_reason) or
2908 return $e->die_event;
2909 if (not_cancelable($result)) { # event not from CStoreEditor
2912 } elsif ($result == -1) {
2914 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
2917 $e->commit or return $e->die_event;
2919 # XXX create purchase order status events?
2921 if ($mgr->{post_commit}) {
2922 foreach my $func (@{$mgr->{post_commit}}) {
2930 sub cancel_purchase_order {
2931 my ($mgr, $po_id, $cancel_reason) = @_;
2933 my $po = $mgr->editor->retrieve_acq_purchase_order($po_id) or return 0;
2935 # XXX is "cancelled" a typo? It's not correct US spelling, anyway.
2936 # Depending on context, this may not warrant an event.
2937 return -1 if $po->state eq "cancelled";
2939 # But this always does.
2940 return new OpenILS::Event(
2941 "ACQ_NOT_CANCELABLE", "note" => "purchase_order $po_id"
2942 ) unless ($po->state eq "on-order" or $po->state eq "pending");
2945 $mgr->editor->allowed("CREATE_PURCHASE_ORDER", $po->ordering_agency);
2947 $po->state("cancelled");
2948 $po->cancel_reason($cancel_reason->id);
2950 my $li_ids = $mgr->editor->search_acq_lineitem(
2951 {"purchase_order" => $po_id}, {"idlist" => 1}
2954 my $result = {"li" => {}, "lid" => {}};
2955 foreach my $li_id (@$li_ids) {
2956 my $li_result = cancel_lineitem($mgr, $li_id, $cancel_reason)
2959 next if $li_result == -1; # already canceled:skip.
2960 return $li_result if not_cancelable($li_result); # not cancelable:stop.
2962 # Merge in each LI result (there's only going to be
2963 # one per call to cancel_lineitem).
2964 my ($k, $v) = each %{$li_result->{"li"}};
2965 $result->{"li"}->{$k} = $v;
2967 # Merge in each LID result (there may be many per call to
2969 while (($k, $v) = each %{$li_result->{"lid"}}) {
2970 $result->{"lid"}->{$k} = $v;
2974 my $po_item_ids = $mgr->editor
2975 ->search_acq_po_item({purchase_order => $po_id}, {idlist => 1});
2977 for my $po_item_id (@$po_item_ids) {
2979 my $po_item = $mgr->editor->retrieve_acq_po_item([
2982 flesh_fields => {acqpoi => ['purchase_order', 'fund_debit']}
2984 ]) or return -1; # results in rollback
2986 # returns undef on success
2987 my $result = clear_po_item($mgr->editor, $po_item);
2989 return $result if not_cancelable($result);
2990 return -1 if $result; # other failure events, results in rollback
2994 # TODO who/what/where/how do we indicate this change for electronic orders?
2995 # TODO return changes to encumbered/spent
2996 # TODO maybe cascade up from smaller object to container object if last
2997 # smaller object in the container has been canceled?
2999 update_purchase_order($mgr, $po) or return 0;
3001 $po_id => {"state" => $po->state, "cancel_reason" => $cancel_reason}
3007 __PACKAGE__->register_method(
3008 method => "cancel_lineitem_api",
3009 api_name => "open-ils.acq.lineitem.cancel",
3011 desc => q/Cancels an on-order lineitem/,
3013 {desc => "Authentication token", type => "string"},
3014 {desc => "Lineitem ID to cancel", type => "number"},
3015 {desc => "Cancel reason ID", type => "number"}
3017 return => {desc => q/Object describing changed LIs and LIDs on success;
3022 __PACKAGE__->register_method(
3023 method => "cancel_lineitem_api",
3024 api_name => "open-ils.acq.lineitem.cancel.batch",
3026 desc => q/Batched version of open-ils.acq.lineitem.cancel/,
3027 return => {desc => q/Object describing changed LIs and LIDs on success;
3032 sub cancel_lineitem_api {
3033 my ($self, $conn, $auth, $li_id, $cancel_reason) = @_;
3035 my $batched = $self->api_name =~ /\.batch/;
3037 my $e = new_editor("xact" => 1, "authtoken" => $auth);
3038 return $e->die_event unless $e->checkauth;
3039 my $mgr = new OpenILS::Application::Acq::BatchManager(
3040 "editor" => $e, "conn" => $conn
3043 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
3044 return new OpenILS::Event(
3045 "BAD_PARAMS", "note" => "Provide cancel reason ID"
3048 my ($result, $maybe_event);
3051 $result = {"li" => {}, "lid" => {}};
3052 foreach my $one_li_id (@$li_id) {
3053 my $one = cancel_lineitem($mgr, $one_li_id, $cancel_reason) or
3054 return $e->die_event;
3055 if (not_cancelable($one)) {
3056 $maybe_event = $one;
3057 } elsif ($result == -1) {
3058 $maybe_event = new OpenILS::Event("ACQ_ALREADY_CANCELED");
3062 while (($k, $v) = each %{$one->{"li"}}) {
3063 $result->{"li"}->{$k} = $v;
3066 if ($one->{"lid"}) {
3067 while (($k, $v) = each %{$one->{"lid"}}) {
3068 $result->{"lid"}->{$k} = $v;
3074 $result = cancel_lineitem($mgr, $li_id, $cancel_reason) or
3075 return $e->die_event;
3077 if (not_cancelable($result)) {
3080 } elsif ($result == -1) {
3082 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3086 if ($batched and not scalar keys %{$result->{"li"}}) {
3088 return $maybe_event;
3090 $e->commit or return $e->die_event;
3091 # create_lineitem_status_events should handle array li_id ok
3092 create_lineitem_status_events($mgr, $li_id, "aur.cancelled");
3094 if ($mgr->{post_commit}) {
3095 foreach my $func (@{$mgr->{post_commit}}) {
3104 sub cancel_lineitem {
3105 my ($mgr, $li_id, $cancel_reason) = @_;
3107 my $li = $mgr->editor->retrieve_acq_lineitem([
3108 $li_id, {flesh => 1,
3109 flesh_fields => {jub => ['purchase_order','cancel_reason']}}
3112 return 0 unless $mgr->editor->allowed(
3113 "CREATE_PURCHASE_ORDER", $li->purchase_order->ordering_agency
3116 # Depending on context, this may not warrant an event.
3117 return -1 if $li->state eq "cancelled"
3118 and $li->cancel_reason->keep_debits eq 'f';
3120 # But this always does. Note that this used to be looser, but you can
3121 # no longer cancel lineitems that lack a PO or that are in "pending-order"
3122 # state (you could in the past).
3123 return new OpenILS::Event(
3124 "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id"
3125 ) unless $li->purchase_order and
3126 ($li->state eq "on-order" or $li->state eq "cancelled");
3128 $li->state("cancelled");
3129 $li->cancel_reason($cancel_reason->id);
3131 my $lids = $mgr->editor->search_acq_lineitem_detail([{
3132 "lineitem" => $li_id
3135 flesh_fields => { acqlid => ['eg_copy_id'] }
3138 my $result = {"lid" => {}};
3140 foreach my $lid (@$lids) {
3141 my $lid_result = cancel_lineitem_detail($mgr, $lid->id, $cancel_reason)
3144 # gathering any real copies for deletion
3145 if ($lid->eg_copy_id) {
3146 $lid->eg_copy_id->isdeleted('t');
3147 push @$copies, $lid->eg_copy_id;
3150 next if $lid_result == -1; # already canceled: just skip it.
3151 return $lid_result if not_cancelable($lid_result); # not cxlable: stop.
3153 # Merge in each LID result (there's only going to be one per call to
3154 # cancel_lineitem_detail).
3155 my ($k, $v) = each %{$lid_result->{"lid"}};
3156 $result->{"lid"}->{$k} = $v;
3159 # Attempt to delete the gathered copies (this will also handle volume deletion and bib deletion)
3160 # Delete empty bibs according org unit setting
3161 my $force_delete_empty_bib = $U->ou_ancestor_setting_value(
3162 $mgr->editor->requestor->ws_ou, 'cat.bib.delete_on_no_copy_via_acq_lineitem_cancel', $mgr->editor);
3163 if (scalar(@$copies)>0) {
3165 my $delete_stats = undef;
3166 my $retarget_holds = [];
3167 my $cat_evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
3168 $mgr->editor, $override, undef, $copies, $delete_stats, $retarget_holds,$force_delete_empty_bib);
3171 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($cat_evt));
3172 return new OpenILS::Event(
3173 "ACQ_NOT_CANCELABLE", "note" => "lineitem $li_id", "payload" => $cat_evt
3177 # We can't do the following and stay within the same transaction, but that's okay, the hold targeter will pick these up later.
3178 #my $ses = OpenSRF::AppSession->create('open-ils.circ');
3179 #$ses->request('open-ils.circ.hold.reset.batch', $auth, $retarget_holds);
3182 # if we have a bib, check to see whether it has been deleted. if so, cancel any active holds targeting that bib
3183 if ($li->eg_bib_id) {
3184 my $bib = $mgr->editor->retrieve_biblio_record_entry($li->eg_bib_id) or return new OpenILS::Event(
3185 "ACQ_NOT_CANCELABLE", "note" => "Could not retrieve bib " . $li->eg_bib_id . " for lineitem $li_id"
3187 if ($U->is_true($bib->deleted)) {
3188 my $holds = $mgr->editor->search_action_hold_request(
3189 { cancel_time => undef,
3190 fulfillment_time => undef,
3191 target => $li->eg_bib_id
3195 my %cached_usr_home_ou = ();
3197 for my $hold (@$holds) {
3199 $logger->info("Cancelling hold ".$hold->id.
3200 " due to acq lineitem cancellation.");
3202 $hold->cancel_time('now');
3203 $hold->cancel_cause(5); # 'Staff forced'--we may want a new hold cancel cause reason for this
3204 $hold->cancel_note('Corresponding Acquistion Lineitem/Purchase Order was cancelled.');
3205 unless($mgr->editor->update_action_hold_request($hold)) {
3206 my $evt = $mgr->editor->event;
3207 $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
3208 return new OpenILS::Event(
3209 "ACQ_NOT_CANCELABLE", "note" => "Could not cancel hold " . $hold->id . " for lineitem $li_id", "payload" => $evt
3212 if (! defined $mgr->{post_commit}) { # we need a mechanism for creating trigger events, but only if the transaction gets committed
3213 $mgr->{post_commit} = [];
3215 push @{ $mgr->{post_commit} }, sub {
3216 my $home_ou = $cached_usr_home_ou{$hold->usr};
3218 my $user = $mgr->editor->retrieve_actor_user($hold->usr); # FIXME: how do we want to handle failures here?
3219 $home_ou = $user->home_ou;
3220 $cached_usr_home_ou{$hold->usr} = $home_ou;
3222 $U->create_events_for_hook('hold_request.cancel.cancelled_order', $hold, $home_ou);
3228 update_lineitem($mgr, $li) or return 0;
3231 "state" => $li->state,
3232 "cancel_reason" => $cancel_reason
3239 __PACKAGE__->register_method(
3240 method => "cancel_lineitem_detail_api",
3241 api_name => "open-ils.acq.lineitem_detail.cancel",
3243 desc => q/Cancels an on-order lineitem detail/,
3245 {desc => "Authentication token", type => "string"},
3246 {desc => "Lineitem detail ID to cancel", type => "number"},
3247 {desc => "Cancel reason ID", type => "number"}
3249 return => {desc => q/Object describing changed LIDs on success;
3254 sub cancel_lineitem_detail_api {
3255 my ($self, $conn, $auth, $lid_id, $cancel_reason) = @_;
3257 my $e = new_editor("xact" => 1, "authtoken" => $auth);
3258 return $e->die_event unless $e->checkauth;
3259 my $mgr = new OpenILS::Application::Acq::BatchManager(
3260 "editor" => $e, "conn" => $conn
3263 $cancel_reason = $mgr->editor->retrieve_acq_cancel_reason($cancel_reason) or
3264 return new OpenILS::Event(
3265 "BAD_PARAMS", "note" => "Provide cancel reason ID"
3268 my $result = cancel_lineitem_detail($mgr, $lid_id, $cancel_reason) or
3269 return $e->die_event;
3271 if (not_cancelable($result)) {
3274 } elsif ($result == -1) {
3276 return new OpenILS::Event("ACQ_ALREADY_CANCELED");
3279 $e->commit or return $e->die_event;
3281 # XXX create lineitem detail status events?
3285 sub cancel_lineitem_detail {
3286 my ($mgr, $lid_id, $cancel_reason) = @_;
3287 my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
3291 "acqlid" => ["lineitem","cancel_reason"],
3292 "jub" => ["purchase_order"]
3297 # It's OK to cancel an already-canceled copy if the copy was
3298 # previously "delayed" -- keep_debits == true
3299 # Depending on context, this may not warrant an event.
3300 return -1 if $lid->cancel_reason
3301 and $lid->cancel_reason->keep_debits eq 'f';
3303 # But this always does.
3304 return new OpenILS::Event(
3305 "ACQ_NOT_CANCELABLE", "note" => "lineitem_detail $lid_id"
3307 (! $lid->lineitem->purchase_order) or
3309 (not $lid->recv_time) and
3311 $lid->lineitem->purchase_order and (
3312 $lid->lineitem->state eq "on-order" or
3313 $lid->lineitem->state eq "pending-order" or
3314 $lid->lineitem->state eq "cancelled"
3319 return 0 unless $mgr->editor->allowed(
3320 "CREATE_PURCHASE_ORDER",
3321 $lid->lineitem->purchase_order->ordering_agency
3322 ) or (! $lid->lineitem->purchase_order);
3324 $lid->cancel_reason($cancel_reason->id);
3326 unless($U->is_true($cancel_reason->keep_debits)) {
3327 my $debit_id = $lid->fund_debit;
3328 $lid->clear_fund_debit;
3331 # item is cancelled. Remove the fund debit.
3332 my $debit = $mgr->editor->retrieve_acq_fund_debit($debit_id);
3333 if (!$U->is_true($debit->encumbrance)) {
3334 $mgr->editor->rollback;
3335 return OpenILS::Event->new('ACQ_NOT_CANCELABLE',
3336 note => "Debit is marked as paid: $debit_id");
3338 $mgr->editor->delete_acq_fund_debit($debit) or return $mgr->editor->die_event;
3342 # XXX LIDs don't have either an editor or a edit_time field. Should we
3343 # update these on the LI when we alter an LID?
3344 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
3346 return {"lid" => {$lid_id => {"cancel_reason" => $cancel_reason}}};
3349 __PACKAGE__->register_method(
3350 method => "delete_po_item_api",
3351 api_name => "open-ils.acq.po_item.delete",
3353 desc => q/Deletes a po_item and removes its debit/,
3355 {desc => "Authentication token", type => "string"},
3356 {desc => "po_item ID to delete", type => "number"},
3358 return => {desc => q/1 on success, Event on error/}
3362 sub delete_po_item_api {
3363 my($self, $client, $auth, $po_item_id) = @_;
3364 my $e = new_editor(authtoken => $auth, xact => 1);
3365 return $e->die_event unless $e->checkauth;
3367 my $po_item = $e->retrieve_acq_po_item([
3370 flesh_fields => {acqpoi => ['purchase_order', 'fund_debit']}
3372 ]) or return $e->die_event;
3374 return $e->die_event unless
3375 $e->allowed('CREATE_PURCHASE_ORDER',
3376 $po_item->purchase_order->ordering_agency);
3378 # remove debit, delete item
3379 my $result = clear_po_item($e, $po_item, 1);
3391 # 1. Removes linked fund debit from a PO item if present and still encumbered.
3392 # 2. Optionally also deletes the po_item object
3393 # po_item is fleshed with purchase_order and fund_debit
3395 my ($e, $po_item, $delete_item) = @_;
3397 if ($po_item->fund_debit) {
3399 if (!$U->is_true($po_item->fund_debit->encumbrance)) {
3400 # debit has been paid. We cannot delete it.
3401 return OpenILS::Event->new('ACQ_NOT_CANCELABLE',
3402 note => "Debit is marked as paid: ".$po_item->fund_debit->id);
3405 # fund_debit is OK to delete.
3406 $e->delete_acq_fund_debit($po_item->fund_debit)
3407 or return $e->die_event;
3411 $e->delete_acq_po_item($po_item) or return $e->die_event;
3413 # remove our link to the now-deleted fund_debit.
3414 $po_item->clear_fund_debit;
3415 $e->update_acq_po_item($po_item) or return $e->die_event;
3422 __PACKAGE__->register_method(
3423 method => 'user_requests',
3424 api_name => 'open-ils.acq.user_request.retrieve.by_user_id',
3427 desc => 'Retrieve fleshed user requests and related data for a given user.',
3429 { desc => 'Authentication token', type => 'string' },
3430 { desc => 'User ID of the owner, or array of IDs', },
3431 { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3436 desc => 'Fleshed user requests and related data',
3442 __PACKAGE__->register_method(
3443 method => 'user_requests',
3444 api_name => 'open-ils.acq.user_request.retrieve.by_home_ou',
3447 desc => 'Retrieve fleshed user requests and related data for a given org unit or units.',
3449 { desc => 'Authentication token', type => 'string' },
3450 { desc => 'Org unit ID, or array of IDs', },
3451 { desc => 'Options hash (optional) with any of the keys: order_by, limit, offset, state (of the lineitem)',
3456 desc => 'Fleshed user requests and related data',
3463 my($self, $conn, $auth, $search_value, $options) = @_;
3464 my $e = new_editor(authtoken => $auth);
3465 return $e->event unless $e->checkauth;
3466 my $rid = $e->requestor->id;
3470 "select"=>{"aur"=>["id"],"au"=>["home_ou", {column => 'id', alias => 'usr_id'} ]},
3471 "from"=>{ "aur" => { "au" => {}, "jub" => { "type" => "left" } } },
3475 {"id"=>undef}, # this with the left-join pulls in requests without lineitems
3476 {"state"=>["new","on-order","pending-order"]} # FIXME - probably needs softcoding
3480 "order_by"=>[{"class"=>"aur", "field"=>"request_date", "direction"=>"desc"}]
3483 foreach (qw/ order_by limit offset /) {
3484 $query->{$_} = $options->{$_} if defined $options->{$_};
3486 if (defined $options->{'state'}) {
3487 $query->{'where'}->{'+jub'}->{'-or'}->[1]->{'state'} = $options->{'state'};
3490 if ($self->api_name =~ /by_user_id/) {
3491 $query->{'where'}->{'usr'} = $search_value;
3493 $query->{'where'}->{'+au'} = { 'home_ou' => $search_value };
3496 my $pertinent_ids = $e->json_query($query);
3499 for my $id_blob (@$pertinent_ids) {
3500 if ($rid != $id_blob->{usr_id}) {
3501 if (!defined $perm_test{ $id_blob->{home_ou} }) {
3502 $perm_test{ $id_blob->{home_ou} } = $e->allowed( ['user_request.view'], $id_blob->{home_ou} );
3504 if (!$perm_test{ $id_blob->{home_ou} }) {
3508 my $aur_obj = $e->retrieve_acq_user_request([
3510 {flesh => 1, flesh_fields => { "aur" => [ 'lineitem' ] } }
3512 if (! $aur_obj) { next; }
3514 if ($aur_obj->lineitem()) {
3515 $aur_obj->lineitem()->clear_marc();
3517 $conn->respond($aur_obj);
3523 __PACKAGE__->register_method (
3524 method => 'update_user_request',
3525 api_name => 'open-ils.acq.user_request.cancel.batch',
3528 desc => 'If given a cancel reason, will update the request with that reason, otherwise, this will delete the request altogether. The ' .
3529 'intention is for staff interfaces or processes to provide cancel reasons, and for patron interfaces to just delete the requests.' ,
3531 { desc => 'Authentication token', type => 'string' },
3532 { desc => 'ID or array of IDs for the user requests to cancel' },
3533 { desc => 'Cancel Reason ID (optional)', type => 'string' }
3536 desc => 'progress object, event on error',
3540 __PACKAGE__->register_method (
3541 method => 'update_user_request',
3542 api_name => 'open-ils.acq.user_request.set_no_hold.batch',
3545 desc => 'Remove the hold from a user request or set of requests',
3547 { desc => 'Authentication token', type => 'string' },
3548 { desc => 'ID or array of IDs for the user requests to modify' }
3551 desc => 'progress object, event on error',
3556 sub update_user_request {
3557 my($self, $conn, $auth, $aur_ids, $cancel_reason) = @_;
3558 my $e = new_editor(xact => 1, authtoken => $auth);
3559 return $e->die_event unless $e->checkauth;
3560 my $rid = $e->requestor->id;
3564 for my $id (@$aur_ids) {
3566 my $aur_obj = $e->retrieve_acq_user_request([
3569 flesh_fields => { "aur" => ['lineitem', 'usr'] }
3571 ]) or return $e->die_event;
3573 my $context_org = $aur_obj->usr()->home_ou();
3574 $aur_obj->usr( $aur_obj->usr()->id() );
3576 if ($rid != $aur_obj->usr) {
3577 if (!defined $perm_test{ $context_org }) {
3578 $perm_test{ $context_org } = $e->allowed( ['user_request.update'], $context_org );
3580 if (!$perm_test{ $context_org }) {
3585 if($self->api_name =~ /set_no_hold/) {
3586 if ($U->is_true($aur_obj->hold)) {
3588 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3592 if($self->api_name =~ /cancel/) {
3593 if ( $cancel_reason ) {
3594 $aur_obj->cancel_reason( $cancel_reason );
3595 $e->update_acq_user_request($aur_obj) or return $e->die_event;
3596 create_user_request_events( $e, [ $aur_obj ], 'aur.rejected' );
3598 $e->delete_acq_user_request($aur_obj);
3602 $conn->respond({maximum => scalar(@$aur_ids), progress => $x++});
3606 return {complete => 1};
3609 __PACKAGE__->register_method (
3610 method => 'new_user_request',
3611 api_name => 'open-ils.acq.user_request.create',
3613 desc => 'Create a new user request object in the DB',
3615 { desc => 'Authentication token', type => 'string' },
3616 { desc => 'User request data hash. Hash keys match the fields for the "aur" object', type => 'object' }
3619 desc => 'The created user request object, or event on error'
3624 sub new_user_request {
3625 my($self, $conn, $auth, $form_data) = @_;
3626 my $e = new_editor(xact => 1, authtoken => $auth);
3627 return $e->die_event unless $e->checkauth;
3628 my $rid = $e->requestor->id;
3629 my $target_user_fleshed;
3630 if (! defined $$form_data{'usr'}) {
3631 $$form_data{'usr'} = $rid;
3633 if ($$form_data{'usr'} != $rid) {
3634 # See if the requestor can place the request on behalf of a different user.
3635 $target_user_fleshed = $e->retrieve_actor_user($$form_data{'usr'}) or return $e->die_event;
3636 $e->allowed('user_request.create', $target_user_fleshed->home_ou) or return $e->die_event;
3638 $target_user_fleshed = $e->requestor;
3639 $e->allowed('CREATE_PURCHASE_REQUEST') or return $e->die_event;
3641 if (! defined $$form_data{'pickup_lib'}) {
3642 if ($target_user_fleshed->ws_ou) {
3643 $$form_data{'pickup_lib'} = $target_user_fleshed->ws_ou;
3645 $$form_data{'pickup_lib'} = $target_user_fleshed->home_ou;
3648 if (! defined $$form_data{'request_type'}) {
3649 $$form_data{'request_type'} = 1; # Books
3651 my $aur_obj = new Fieldmapper::acq::user_request;
3653 $aur_obj->usr( $$form_data{'usr'} );
3654 $aur_obj->request_date( 'now' );
3655 for my $field ( keys %$form_data ) {
3656 if (defined $$form_data{$field} and $field !~ /^(id|lineitem|eg_bib|request_date|cancel_reason)$/) {
3657 $aur_obj->$field( $$form_data{$field} );
3661 $aur_obj = $e->create_acq_user_request($aur_obj) or return $e->die_event;
3663 $e->commit and create_user_request_events( $e, [ $aur_obj ], 'aur.created' );
3668 sub create_user_request_events {
3669 my($e, $user_reqs, $hook) = @_;
3671 my $ses = OpenSRF::AppSession->create('open-ils.trigger');
3674 my %cached_usr_home_ou = ();
3675 for my $user_req (@$user_reqs) {
3676 my $home_ou = $cached_usr_home_ou{$user_req->usr};
3678 my $user = $e->retrieve_actor_user($user_req->usr) or return $e->die_event;
3679 $home_ou = $user->home_ou;
3680 $cached_usr_home_ou{$user_req->usr} = $home_ou;
3682 my $req = $ses->request('open-ils.trigger.event.autocreate', $hook, $user_req, $home_ou);
3691 __PACKAGE__->register_method(
3692 method => "po_note_CUD_batch",
3693 api_name => "open-ils.acq.po_note.cud.batch",
3696 desc => q/Manage purchase order notes/,
3698 {desc => "Authentication token", type => "string"},
3699 {desc => "List of po_notes to manage", type => "array"},
3701 return => {desc => "Stream of successfully managed objects"}
3705 sub po_note_CUD_batch {
3706 my ($self, $conn, $auth, $notes) = @_;
3708 my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3709 return $e->die_event unless $e->checkauth;
3712 my $total = @$notes;
3715 foreach my $note (@$notes) {
3717 $note->editor($e->requestor->id);
3718 $note->edit_time("now");
3721 $note->creator($e->requestor->id);
3722 $note = $e->create_acq_po_note($note) or return $e->die_event;
3723 } elsif ($note->isdeleted) {
3724 $e->delete_acq_po_note($note) or return $e->die_event;
3725 } elsif ($note->ischanged) {
3726 $e->update_acq_po_note($note) or return $e->die_event;
3729 unless ($note->isdeleted) {
3730 $note = $e->retrieve_acq_po_note($note->id) or
3731 return $e->die_event;
3735 {"maximum" => $total, "progress" => ++$count, "note" => $note}
3739 $e->commit and $conn->respond_complete or return $e->die_event;
3743 # retrieves a lineitem, fleshes its PO and PL, checks perms
3744 # returns ($li, $evt, $org)
3745 sub fetch_and_check_li {
3748 my $perm_mode = shift || 'read';
3750 my $li = $e->retrieve_acq_lineitem([
3753 flesh_fields => {jub => ['purchase_order', 'picklist']}
3755 ]) or return (undef, $e->die_event);
3758 if(my $po = $li->purchase_order) {
3759 $org = $po->ordering_agency;
3760 my $perms = ($perm_mode eq 'read') ? 'VIEW_PURCHASE_ORDER' : 'CREATE_PURCHASE_ORDER';
3761 return ($li, $e->die_event) unless $e->allowed($perms, $org);
3763 } elsif(my $pl = $li->picklist) {
3764 $org = $pl->org_unit;
3765 my $perms = ($perm_mode eq 'read') ? 'VIEW_PICKLIST' : 'CREATE_PICKLIST';
3766 return ($li, $e->die_event) unless $e->allowed($perms, $org);
3769 return ($li, undef, $org);
3773 __PACKAGE__->register_method(
3774 method => "clone_distrib_form",
3775 api_name => "open-ils.acq.distribution_formula.clone",
3778 desc => q/Clone a distribution formula/,
3780 {desc => "Authentication token", type => "string"},
3781 {desc => "Original formula ID", type => 'integer'},
3782 {desc => "Name of new formula", type => 'string'},
3784 return => {desc => "ID of newly created formula"}
3788 sub clone_distrib_form {
3789 my($self, $client, $auth, $form_id, $new_name) = @_;
3791 my $e = new_editor("xact"=> 1, "authtoken" => $auth);
3792 return $e->die_event unless $e->checkauth;
3794 my $old_form = $e->retrieve_acq_distribution_formula($form_id) or return $e->die_event;
3795 return $e->die_event unless $e->allowed('ADMIN_ACQ_DISTRIB_FORMULA', $old_form->owner);
3797 my $new_form = Fieldmapper::acq::distribution_formula->new;
3799 $new_form->owner($old_form->owner);
3800 $new_form->name($new_name);
3801 $e->create_acq_distribution_formula($new_form) or return $e->die_event;
3803 my $entries = $e->search_acq_distribution_formula_entry({formula => $form_id});
3804 for my $entry (@$entries) {
3805 my $new_entry = Fieldmapper::acq::distribution_formula_entry->new;
3806 $new_entry->$_($entry->$_()) for $entry->real_fields;
3807 $new_entry->formula($new_form->id);
3808 $new_entry->clear_id;
3809 $e->create_acq_distribution_formula_entry($new_entry) or return $e->die_event;
3813 return $new_form->id;
3816 __PACKAGE__->register_method(
3817 method => 'add_li_to_po',
3818 api_name => 'open-ils.acq.purchase_order.add_lineitem',
3820 desc => q/Adds a lineitem to an existing purchase order/,
3822 {desc => 'Authentication token', type => 'string'},
3823 {desc => 'The purchase order id', type => 'number'},
3824 {desc => 'The lineitem ID (or an array of them)', type => 'mixed'},
3826 return => {desc => 'Streams a total versus completed counts object, event on error'}
3831 my($self, $conn, $auth, $po_id, $li_id) = @_;
3833 my $e = new_editor(authtoken => $auth, xact => 1);
3834 return $e->die_event unless $e->checkauth;
3836 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
3838 my $po = $e->retrieve_acq_purchase_order($po_id)
3839 or return $e->die_event;
3841 return $e->die_event unless
3842 $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
3844 unless ($po->state =~ /new|pending/) {
3846 return {success => 0, po => $po, error => 'bad-po-state'};
3851 if (ref $li_id eq "ARRAY") {
3852 $li_id = [ map { int($_) } @$li_id ];
3853 return $e->die_event(new OpenILS::Event("BAD_PARAMS")) unless @$li_id;
3855 $lis = $e->search_acq_lineitem({id => $li_id})
3856 or return $e->die_event;
3858 my $li = $e->retrieve_acq_lineitem(int($li_id))
3859 or return $e->die_event;
3863 foreach my $li (@$lis) {
3864 if ($li->state !~ /new|order-ready|pending-order/ or
3865 $li->purchase_order) {
3867 return {success => 0, li => $li, error => 'bad-li-state'};
3870 $li->provider($po->provider);
3871 $li->purchase_order($po_id);
3872 $li->state('pending-order');
3873 apply_default_copies($mgr, $po, $li->id) or return $e->die_event;
3874 update_lineitem($mgr, $li) or return $e->die_event;
3878 return {success => 1};
3881 __PACKAGE__->register_method(
3882 method => 'po_lineitems_no_copies',
3883 api_name => 'open-ils.acq.purchase_order.no_copy_lineitems.id_list',
3887 desc => q/Returns the set of lineitem IDs for a given PO that have no copies attached/,
3889 {desc => 'Authentication token', type => 'string'},
3890 {desc => 'The purchase order id', type => 'number'},
3892 return => {desc => 'Stream of lineitem IDs on success, event on error'}
3896 sub po_lineitems_no_copies {
3897 my ($self, $conn, $auth, $po_id) = @_;
3899 my $e = new_editor(authtoken => $auth);
3900 return $e->event unless $e->checkauth;
3902 # first check the view perms for LI's attached to this PO
3903 my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->event;
3904 return $e->event unless $e->allowed('VIEW_PURCHASE_ORDER', $po->ordering_agency);
3906 my $ids = $e->json_query({
3907 select => {jub => ['id']},
3908 from => {jub => {acqlid => {type => 'left'}}},
3910 '+jub' => {purchase_order => $po_id},
3911 '+acqlid' => {lineitem => undef}
3915 $conn->respond($_->{id}) for @$ids;
3919 __PACKAGE__->register_method(
3920 method => 'set_li_order_ident',
3921 api_name => 'open-ils.acq.lineitem.order_identifier.set',
3924 Given an existing lineitem_attr (typically a marc_attr), this will
3925 create a matching local_attr to store the name and value and mark
3926 the attr as the order_ident. Any existing local_attr marked as
3927 order_ident is removed.
3930 {desc => 'Authentication token', type => 'string'},
3931 {desc => q/Args object:
3932 source_attr_id : ID of the existing lineitem_attr to use as
3934 lineitem_id : lineitem id
3935 attr_name : name ('isbn', etc.) of a new marc_attr to add to
3936 the lineitem to use for the order ident
3937 attr_value : value for the new marc_attr
3938 no_apply_bre : if set, newly added attrs will not be applied
3939 to the lineitems' linked bib record/,
3942 return => {desc => q/Returns the attribute
3943 responsible for tracking the order identifier/}
3947 sub set_li_order_ident {
3948 my ($self, $conn, $auth, $args) = @_;
3952 my $source_attr_id = $args->{source_attr_id};
3954 my $e = new_editor(authtoken => $auth, xact => 1);
3955 return $e->die_event unless $e->checkauth;
3957 # fetch attr, LI, and check update permissions
3959 my $li_id = $args->{lineitem_id};
3961 if ($source_attr_id) {
3962 $source_attr = $e->retrieve_acq_lineitem_attr($source_attr_id)
3963 or return $e->die_event;
3964 $li_id = $source_attr->lineitem;
3967 my ($li, $evt, $perm_org) = fetch_and_check_li($e, $li_id, 'write');
3968 return $evt if $evt;
3970 return $e->die_event unless
3971 $e->allowed('ACQ_SET_LINEITEM_IDENTIFIER', $perm_org);
3973 # if needed, create a new marc attr for
3974 # the lineitem to represent the ident value
3976 ($source_attr, $evt) = apply_new_li_ident_attr(
3977 $e, $li, $perm_org, $args->{attr_name}, $args->{attr_value})
3978 unless $source_attr;
3980 return $evt if $evt;
3982 # remove the existing order_ident attribute if present
3984 my $old_attr = $e->search_acq_lineitem_attr({
3985 attr_type => 'lineitem_local_attr_definition',
3986 lineitem => $li->id,
3992 # if we already have an order_ident that matches the
3993 # source attr, there's nothing left to do.
3995 if ($old_attr->attr_name eq $source_attr->attr_name and
3996 $old_attr->attr_value eq $source_attr->attr_value) {
4002 # remove the old order_ident attribute
4003 $e->delete_acq_lineitem_attr($old_attr) or return $e->die_event;
4007 # make sure we have a local_attr_def to match the source attr def
4009 my $local_def = $e->search_acq_lineitem_local_attr_definition({
4010 code => $source_attr->attr_name
4015 $e->retrieve_acq_lineitem_attr_definition($source_attr->definition);
4016 $local_def = Fieldmapper::acq::lineitem_local_attr_definition->new;
4017 $local_def->code($source_def->code);
4018 $local_def->description($source_def->description);
4019 $local_def = $e->create_acq_lineitem_local_attr_definition($local_def)
4020 or return $e->die_event;
4023 # create the new order_ident local attr
4025 my $new_attr = Fieldmapper::acq::lineitem_attr->new;
4026 $new_attr->definition($local_def->id);
4027 $new_attr->attr_type('lineitem_local_attr_definition');
4028 $new_attr->lineitem($li->id);
4029 $new_attr->attr_name($source_attr->attr_name);
4030 $new_attr->attr_value($source_attr->attr_value);
4031 $new_attr->order_ident('t');
4033 $new_attr = $e->create_acq_lineitem_attr($new_attr)
4034 or return $e->die_event;
4041 # Given an isbn, issn, or upc, add the value to the lineitem marc.
4042 # Upon update, the value will be auto-magically represented as
4043 # a lineitem marc attr.
4044 # If the li is linked to a bib record and the user has the correct
4045 # permissions, update the bib record to match.
4046 sub apply_new_li_ident_attr {
4047 my ($e, $li, $perm_org, $attr_name, $attr_value) = @_;
4055 my $marc_field = MARC::Field->new(
4056 $tags{$attr_name}, '', '','a' => $attr_value);
4058 my $li_rec = MARC::Record->new_from_xml($li->marc, 'UTF-8', 'USMARC');
4059 $li_rec->insert_fields_ordered($marc_field);
4061 $li->marc(clean_marc($li_rec));
4062 $li->editor($e->requestor->id);
4063 $li->edit_time('now');
4065 $e->update_acq_lineitem($li) or return (undef, $e->die_event);
4067 my $source_attr = $e->search_acq_lineitem_attr({
4068 attr_name => $attr_name,
4069 attr_value => $attr_value,
4070 attr_type => 'lineitem_marc_attr_definition'
4073 if (!$source_attr) {
4074 $logger->error("ACQ lineitem update failed to produce a matching ".
4075 " marc attribute for $attr_name => $attr_value");
4076 return (undef, OpenILS::Event->new('INTERNAL_SERVER_ERROR'));
4079 return ($source_attr) unless
4081 $e->allowed('ACQ_ADD_LINEITEM_IDENTIFIER', $perm_org);
4083 # li is linked to a bib record and user has the update perms
4085 my $bre = $e->retrieve_biblio_record_entry($li->eg_bib_id);
4086 my $bre_marc = MARC::Record->new_from_xml($bre->marc, 'UTF-8', 'USMARC');
4087 $bre_marc->insert_fields_ordered($marc_field);
4089 $bre->marc(clean_marc($bre_marc));
4090 $bre->editor($e->requestor->id);
4091 $bre->edit_date('now');
4093 $e->update_biblio_record_entry($bre) or return (undef, $e->die_event);
4095 return ($source_attr);
4098 __PACKAGE__->register_method(
4099 method => 'li_existing_copies',
4100 api_name => 'open-ils.acq.lineitem.existing_copies.count',
4104 Returns the number of catalog copies (acp) which are children of
4105 the same bib record linked to by the given lineitem and which
4106 are owned at or below the lineitem context org unit.
4107 Copies with the following statuses are not counted:
4108 Lost, Missing, Discard Weed, and Lost and Paid.
4111 {desc => 'Authentication token', type => 'string'},
4112 {desc => 'Lineitem ID', type => 'number'}
4114 return => {desc => q/Count or event on error/}
4118 sub li_existing_copies {
4119 my ($self, $client, $auth, $li_id) = @_;
4120 my $e = new_editor("authtoken" => $auth);
4121 return $e->die_event unless $e->checkauth;
4123 my ($li, $evt, $org) = fetch_and_check_li($e, $li_id);
4126 # No fuzzy matching here (e.g. on ISBN). Only exact matches are supported.
4127 return 0 unless $li->eg_bib_id;
4129 my $counts = $e->json_query({
4130 select => {acp => [{
4132 transform => 'count',
4139 field => 'eg_copy_id',
4142 acn => {join => {bre => {}}}
4146 '+bre' => {id => $li->eg_bib_id},
4147 # don't count copies linked to the lineitem in question
4150 {lineitem => undef},
4151 {lineitem => {'<>' => $li_id}}
4155 owning_lib => $U->get_org_descendants($org)
4157 # NOTE: should the excluded copy statuses be an AOUS?
4158 '+acp' => {status => {'not in' => [3, 4, 13, 17]}}
4162 return $counts->[0]->{id};