1 package OpenILS::Application::Acq::BatchManager;
2 use strict; use warnings;
5 my($class, %args) = @_;
6 my $self = bless(\%args, $class);
13 purchase_order => undef,
22 $self->{conn} = $val if $val;
26 my($self, %other_args) = @_;
27 $self->conn->respond({ %{$self->{args}}, %other_args });
29 sub respond_complete {
30 my($self, %other_args) = @_;
32 $self->conn->respond_complete({ %{$self->{args}}, %other_args });
37 $self->{total} = $val if defined $val;
38 return $self->{total};
42 $self->{purchase_order} = $val if $val;
47 $self->{picklist} = $val if $val;
52 $self->{args}->{lid} += 1;
53 $self->{args}->{progress} += 1;
58 $self->{args}->{li} += 1;
59 $self->{args}->{progress} += 1;
64 $self->{args}->{copies} += 1;
65 $self->{args}->{progress} += 1;
69 my($self, $amount) = @_;
70 $self->{args}->{debits_accrued} += $amount;
71 $self->{args}->{progress} += 1;
75 my($self, $editor) = @_;
76 $self->{editor} = $editor if defined $editor;
77 return $self->{editor};
81 $self->{args}->{complete} = 1;
86 package OpenILS::Application::Acq::Order;
87 use base qw/OpenILS::Application/;
88 use strict; use warnings;
89 # ----------------------------------------------------------------------------
90 # Break up each component of the order process and pieces into managable
91 # actions that can be shared across different workflows
92 # ----------------------------------------------------------------------------
94 use OpenSRF::Utils::Logger qw(:logger);
95 use OpenILS::Utils::Fieldmapper;
96 use OpenILS::Utils::CStoreEditor q/:funcs/;
97 use OpenILS::Const qw/:const/;
98 use OpenILS::Application::AppUtils;
99 use OpenILS::Application::Cat::BibCommon;
100 use OpenILS::Application::Cat::AssetCommon;
101 my $U = 'OpenILS::Application::AppUtils';
104 # ----------------------------------------------------------------------------
106 # ----------------------------------------------------------------------------
107 sub create_lineitem {
108 my($mgr, %args) = @_;
109 my $li = Fieldmapper::acq::lineitem->new;
110 $li->creator($mgr->editor->requestor->id);
111 $li->selector($li->creator);
112 $li->editor($li->creator);
113 $li->create_time('now');
114 $li->edit_time('now');
116 $li->$_($args{$_}) for keys %args;
118 return 0 unless update_picklist($mgr, $li->picklist);
121 return $mgr->editor->create_acq_lineitem($li);
124 sub update_lineitem {
126 $li->edit_time('now');
127 $li->editor($mgr->editor->requestor->id);
128 return $li if $mgr->editor->update_acq_lineitem($li);
132 sub delete_lineitem {
134 $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
137 return 0 unless update_picklist($mgr, $li->picklist);
140 if($li->purchase_order) {
141 return 0 unless update_purchase_order($mgr, $li->purchase_order);
144 # delete the attached lineitem_details
145 my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
146 for my $lid_id (@$lid_ids) {
147 return 0 unless delete_lineitem_detail($mgr, undef, $lid_id);
150 return $mgr->editor->delete_acq_lineitem($li);
153 # ----------------------------------------------------------------------------
155 # ----------------------------------------------------------------------------
156 sub create_lineitem_detail {
157 my($mgr, %args) = @_;
158 my $lid = Fieldmapper::acq::lineitem_detail->new;
159 $lid->$_($args{$_}) for keys %args;
161 # create some default values
162 unless($lid->barcode) {
163 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
164 $lid->barcode($pfx.$lid->id);
167 unless($lid->cn_label) {
168 my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
169 $lid->cn_label($pfx.$lid->id);
172 if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
173 $lid->location($loc);
176 my $li = $mgr->editor->retrieve_acq_lineitem($lid->lineitem) or return 0;
177 return 0 unless update_lineitem($mgr, $li);
178 return $mgr->editor->create_acq_lineitem_detail($lid);
181 sub delete_lineitem_detail {
183 $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
184 return $mgr->editor->delete_acq_lineitem_detail($lid);
187 # ----------------------------------------------------------------------------
189 # ----------------------------------------------------------------------------
190 sub create_picklist {
191 my($mgr, %args) = @_;
192 my $picklist = Fieldmapper::acq::picklist->new;
193 $picklist->creator($mgr->editor->requestor->id);
194 $picklist->owner($picklist->creator);
195 $picklist->editor($picklist->creator);
196 $picklist->create_time('now');
197 $picklist->edit_time('now');
198 $picklist->org_unit($mgr->editor->requestor->ws_ou);
199 $picklist->owner($mgr->editor->requestor->id);
200 $picklist->$_($args{$_}) for keys %args;
201 $mgr->picklist($picklist);
202 return $mgr->editor->create_acq_picklist($picklist);
205 sub update_picklist {
206 my($mgr, $picklist) = @_;
207 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
208 $picklist->edit_time('now');
209 $picklist->editor($mgr->editor->requestor->id);
210 return $picklist if $mgr->editor->update_acq_picklist($picklist);
214 sub delete_picklist {
215 my($mgr, $picklist) = @_;
216 $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
218 # delete all 'new' lineitems
219 my $lis = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => 'new'});
221 return 0 unless delete_lineitem($mgr, $li);
224 # detach all non-'new' lineitems
225 $lis = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}});
228 return 0 unless update_lineitem($li);
231 # remove any picklist-specific object perms
232 my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => "".$picklist->id});
234 return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
237 return $mgr->editor->delete_acq_picklist($picklist);
240 # ----------------------------------------------------------------------------
242 # ----------------------------------------------------------------------------
243 sub update_purchase_order {
245 $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
246 $po->editor($mgr->editor->requestor->id);
247 $po->edit_date('now');
248 return $po if $mgr->editor->update_acq_purchase_order($po);
252 sub create_purchase_order {
253 my($mgr, %args) = @_;
254 my $po = Fieldmapper::acq::purchase_order->new;
255 $po->creator($mgr->editor->requestor->id);
256 $po->editor($mgr->editor->requestor->id);
257 $po->owner($mgr->editor->requestor->id);
258 $po->edit_time('now');
259 $po->create_time('now');
260 $po->ordering_agency($mgr->editor->requestor->ws_ou);
261 $po->$_($args{$_}) for keys %args;
262 return $mgr->editor->create_acq_purchase_order($po);
266 # ----------------------------------------------------------------------------
267 # Bib, Callnumber, and Copy data
268 # ----------------------------------------------------------------------------
270 sub create_lineitem_assets {
271 my($mgr, $li_id) = @_;
274 my $li = $mgr->editor->retrieve_acq_lineitem([
277 flesh_fields => {jub => ['purchase_order', 'attributes']}
281 # -----------------------------------------------------------------
282 # first, create the bib record if necessary
283 # -----------------------------------------------------------------
284 unless($li->eg_bib_id) {
285 create_bib($mgr, $li) or return 0;
288 my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
290 # -----------------------------------------------------------------
291 # for each lineitem_detail, create the volume if necessary, create
292 # a copy, and link them all together.
293 # -----------------------------------------------------------------
295 for my $lid_id (@{$li_details}) {
297 my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
298 my $org = $lid->owning_lib;
299 my $label = $lid->cn_label;
301 $cache{$org} = {} unless $cache{$org};
302 my $volume = $cache{$org}{$label};
304 $volume = $cache{$org}{$label} = create_volume($li, $lid) or return 0;
306 create_copy($mgr, $volume, $lid) or return 0;
315 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
316 $mgr->editor, $li->marc, undef, undef, undef, 1); #$rec->bib_source
318 if($U->event_code($record)) {
319 $mgr->editor->event($record);
320 $mgr->editor->rollback;
324 $li->eg_bib_id($record->id);
325 return update_lineitem($mgr, $li);
329 my($mgr, $li, $lid) = @_;
332 OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
340 $mgr->editor->event($evt);
348 my($mgr, $volume, $lid) = @_;
349 my $copy = Fieldmapper::asset::copy->new;
351 $copy->loan_duration(2);
352 $copy->fine_level(2);
353 $copy->status(OILS_COPY_STATUS_ON_ORDER);
354 $copy->barcode($lid->barcode);
355 $copy->location($lid->location);
356 $copy->call_number($volume->id);
357 $copy->circ_lib($volume->owning_lib);
358 $copy->circ_modifier('book'); # XXX
360 my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
362 $mgr->editor->event($evt);
367 $lid->eg_copy_id($copy->id);
368 $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
376 # ----------------------------------------------------------------------------
377 # Workflow: Build a selection list from a Z39.50 search
378 # ----------------------------------------------------------------------------
380 __PACKAGE__->register_method(
382 api_name => 'open-ils.acq.picklist.search.z3950',
385 desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
387 {desc => 'Authentication token', type => 'string'},
388 {desc => 'Search definition', type => 'object'},
389 {desc => 'Picklist name, optional', type => 'string'},
395 my($self, $conn, $auth, $search, $name, $options) = @_;
396 my $e = new_editor(authtoken=>$auth);
397 return $e->event unless $e->checkauth;
398 return $e->event unless $e->allowed('CREATE_PICKLIST');
400 $search->{limit} ||= 10;
403 my $ses = OpenSRF::AppSession->create('open-ils.search');
404 my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
409 while(my $resp = $req->recv(timeout=>60)) {
412 my $e = new_editor(requestor=>$e->requestor, xact=>1);
413 $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
414 $picklist = zsearch_build_pl($mgr, $name);
418 my $result = $resp->content;
419 my $count = $result->{count};
420 $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
422 for my $rec (@{$result->{records}}) {
424 my $li = create_lineitem($mgr,
425 picklist => $picklist->id,
426 source_label => $result->{service},
427 marc => $rec->{marcxml},
428 eg_bib_id => $rec->{bibid}
431 if($$options{respond_li}) {
432 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
433 if $$options{flesh_attrs};
434 $li->clear_marc if $$options{clear_marc};
435 $mgr->respond(lineitem => $li);
442 $mgr->editor->commit;
443 return $mgr->respond_complete;
446 sub zsearch_build_pl {
447 my($mgr, $name) = @_;
450 my $picklist = $mgr->editor->search_acq_picklist({
451 owner => $mgr->editor->requestor->id,
455 if($name eq '' and $picklist) {
456 return 0 unless delete_picklist($mgr, $picklist);
460 return update_picklist($mgr, $picklist) if $picklist;
461 return create_picklist($mgr, name => $name);
465 # ----------------------------------------------------------------------------
466 # Workflow: Build a selection list / PO by importing a batch of MARC records
467 # ----------------------------------------------------------------------------
469 __PACKAGE__->register_method(
470 method => 'upload_records',
471 api_name => 'open-ils.acq.process_upload_records',
477 my($self, $conn, $auth, $key) = @_;
479 my $e = new_editor(authtoken => $auth, xact => 1);
480 return $e->die_event unless $e->checkauth;
481 my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
483 my $cache = OpenSRF::Utils::Cache->new;
486 my $data = $cache->get_cache("vandelay_import_spool_$key");
487 my $purpose = $data->{purpose};
488 my $filename = $data->{path};
489 my $provider = $data->{provider};
490 my $picklist = $data->{picklist};
491 my $create_po = $data->{create_po};
492 my $ordering_agency = $data->{ordering_agency};
495 unless(-r $filename) {
496 $logger->error("unable to read MARC file $filename");
498 return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
501 $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
504 $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
505 if($picklist->owner != $e->requestor->id) {
506 return $e->die_event unless
507 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
512 $purchase_order = Fieldmapper::acq::purchase_order->new;
513 $purchase_order->provider($provider->id);
514 $purchase_order->ordering_agency($ordering_agency);
515 $evt = OpenILS::Application::Acq::Financials::create_purchase_order_impl($e, $purchase_order);
519 $logger->info("acq processing MARC file=$filename");
521 my $marctype = 'USMARC'; # ?
522 my $batch = new MARC::Batch ($marctype, $filename);
530 $logger->info("processing record $count");
534 } catch Error with { $r = -1; };
539 $logger->warn("Proccessing of record $count in set $key failed. Skipping this record");
546 (my $xml = $r->as_xml_record()) =~ s/\n//sog;
547 $xml =~ s/^<\?xml.+\?\s*>//go;
548 $xml =~ s/>\s+</></go;
549 $xml =~ s/\p{Cc}//go;
550 $xml = $U->entityize($xml);
551 $xml =~ s/[\x00-\x1f]//go;
553 my $li = Fieldmapper::acq::lineitem->new;
554 $li->picklist($picklist->id) if $picklist;
555 $li->purchase_order($purchase_order->id) if $purchase_order;
556 $li->source_label($provider->code); # XXX ??
557 $li->provider($provider->id);
558 $li->selector($e->requestor->id);
559 $li->creator($e->requestor->id);
560 $li->editor($e->requestor->id);
561 $li->edit_time('now');
562 $li->create_time('now');
564 $li->state('on-order') if $purchase_order;
565 $e->create_acq_lineitem($li) or die $e->die_event;
567 $conn->respond({count => $count}) if (++$count % 5) == 0;
569 $evt = create_lineitem_details($conn, \$count, $e, $ordering_agency, $li, $purchase_order);
570 die $evt if $evt; # caught below
574 $logger->warn("Encountered a bad record at Vandelay ingest: ".$error);
580 $cache->delete_cache('vandelay_import_spool_' . $key);
582 # clear the cached funds
583 delete $fund_code_map{$_} for keys %fund_code_map;
587 purchase_order => $purchase_order,
588 picklist => $picklist
592 sub create_lineitem_details {
593 my($conn, $countref, $e, $ordering_agency, $li, $purchase_order) = @_;
595 my $holdings = $e->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
596 return undef unless @$holdings;
597 my $org_path = $U->get_org_ancestors($ordering_agency);
601 my $compiled = extract_lineitem_detail_data($e, $org_path, $holdings, $idx);
602 last unless $compiled;
604 for(1..$$compiled{quantity}) {
605 my $lid = Fieldmapper::acq::lineitem_detail->new;
606 $lid->lineitem($li->id);
607 $lid->owning_lib($$compiled{owning_lib});
608 $lid->cn_label($$compiled{call_number});
609 $lid->fund($$compiled{fund});
611 if($purchase_order) {
621 sub extract_lineitem_detail_data {
622 my($e, $org_path, $holdings, $holding_index) = @_;
624 my @data_list = { grep { $_->holding eq $holding_index } @$holdings };
625 my %compiled = map { $_->{attr} => $_->{data} } @data_list;
626 my $err_evt = OpenILS::Event->new('ACQ_IMPORT_ERROR');
628 $compiled{quantity} ||= 1;
630 # ----------------------------------------------------
632 if(my $code = $compiled{fund_code}) {
634 my $fund = $fund_code_map{$code};
636 # search up the org tree for the most appropriate fund
637 for my $org (@$org_path) {
638 $fund = $e->search_acq_fund({org => $org, code => $code, year => DateTime->now->year})->[0];
642 $logger->error("Import error: there is no fund with code $code at orgs $org_path");
647 $compiled{fund} = $fund->id;
648 $fund_code_map{$code} = $fund;
651 # XXX perhaps a default fund?
652 $logger->error("Import error: no fund code provided");
657 $compiled{owning_lib} = $e->search_actor_org_unit({shortname => $compiled{owning_lib}})->[0]
658 or return $e->die_event;
660 # ----------------------------------------------------
661 # find the collection code