]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Acq/Order.pm
move po/lid receive process into order.pm. added receive operation for lineitem
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Acq / Order.pm
1 package OpenILS::Application::Acq::BatchManager;
2 use OpenSRF::AppSession;
3 use OpenSRF::EX qw/:try/;
4 use strict; use warnings;
5
6 sub new {
7     my($class, %args) = @_;
8     my $self = bless(\%args, $class);
9     $self->{args} = {
10         lid => 0,
11         li => 0,
12         copies => 0,
13         bibs => 0,
14         progress => 0,
15         debits_accrued => 0,
16         purchase_order => undef,
17         picklist => undef,
18         complete => 0,
19         indexed => 0,
20         total => 0
21     };
22     $self->{ingest_queue} = [];
23     $self->{cache} = {};
24     $self->throttle(5) unless $self->throttle;
25     return $self;
26 }
27
28 sub conn {
29     my($self, $val) = @_;
30     $self->{conn} = $val if $val;
31     return $self->{conn};
32 }
33 sub throttle {
34     my($self, $val) = @_;
35     $self->{throttle} = $val if $val;
36     return $self->{throttle};
37 }
38 sub respond {
39     my($self, %other_args) = @_;
40     if($self->throttle and not %other_args) {
41         return unless (
42             ($self->{args}->{progress} - $self->{last_respond_progress}) >= $self->throttle
43         );
44     }
45     $self->conn->respond({ %{$self->{args}}, %other_args });
46     $self->{last_respond_progress} = $self->{args}->{progress};
47 }
48 sub respond_complete {
49     my($self, %other_args) = @_;
50     $self->complete;
51     $self->conn->respond_complete({ %{$self->{args}}, %other_args });
52     return undef;
53 }
54 sub total {
55     my($self, $val) = @_;
56     $self->{args}->{total} = $val if defined $val;
57     $self->{args}->{maximum} = $self->{args}->{total};
58     return $self->{args}->{total};
59 }
60 sub purchase_order {
61     my($self, $val) = @_;
62     $self->{args}->{purchase_order} = $val if $val;
63     return $self;
64 }
65 sub picklist {
66     my($self, $val) = @_;
67     $self->{args}->{picklist} = $val if $val;
68     return $self;
69 }
70 sub add_lid {
71     my $self = shift;
72     $self->{args}->{lid} += 1;
73     $self->{args}->{progress} += 1;
74     return $self;
75 }
76 sub add_li {
77     my $self = shift;
78     $self->{args}->{li} += 1;
79     $self->{args}->{progress} += 1;
80     return $self;
81 }
82 sub add_copy {
83     my $self = shift;
84     $self->{args}->{copies} += 1;
85     $self->{args}->{progress} += 1;
86     return $self;
87 }
88 sub add_bib {
89     my $self = shift;
90     $self->{args}->{bibs} += 1;
91     $self->{args}->{progress} += 1;
92     return $self;
93 }
94 sub add_debit {
95     my($self, $amount) = @_;
96     $self->{args}->{debits_accrued} += $amount;
97     $self->{args}->{progress} += 1;
98     return $self;
99 }
100 sub editor {
101     my($self, $editor) = @_;
102     $self->{editor} = $editor if defined $editor;
103     return $self->{editor};
104 }
105 sub complete {
106     my $self = shift;
107     $self->{args}->{complete} = 1;
108     return $self;
109 }
110
111 sub ingest_ses {
112     my($self, $val) = @_;
113     $self->{ingest_ses} = $val if $val;
114     return $self->{ingest_ses};
115 }
116
117 sub push_ingest_queue {
118     my($self, $rec_id) = @_;
119
120     $self->ingest_ses(OpenSRF::AppSession->connect('open-ils.ingest'))
121         unless $self->ingest_ses;
122
123     my $req = $self->ingest_ses->request('open-ils.ingest.full.biblio.record', $rec_id);
124
125     push(@{$self->{ingest_queue}}, $req);
126 }
127
128 sub process_ingest_records {
129     my $self = shift;
130     return unless @{$self->{ingest_queue}};
131
132     for my $req (@{$self->{ingest_queue}}) {
133
134         try { 
135             $req->gather(1); 
136             $self->{args}->{indexed} += 1;
137             $self->{args}->{progress} += 1;
138         } otherwise {};
139
140         $self->respond;
141     }
142     $self->ingest_ses->disconnect;
143 }
144
145
146 sub cache {
147     my($self, $org, $key, $val) = @_;
148     $self->{cache}->{$org} = {} unless $self->{cache}->{org};
149     $self->{cache}->{$org}->{$key} = $val if defined $val;
150     return $self->{cache}->{$org}->{$key};
151 }
152
153
154 package OpenILS::Application::Acq::Order;
155 use base qw/OpenILS::Application/;
156 use strict; use warnings;
157 # ----------------------------------------------------------------------------
158 # Break up each component of the order process and pieces into managable
159 # actions that can be shared across different workflows
160 # ----------------------------------------------------------------------------
161 use OpenILS::Event;
162 use OpenSRF::Utils::Logger qw(:logger);
163 use OpenSRF::Utils::JSON;
164 use OpenILS::Utils::Fieldmapper;
165 use OpenILS::Utils::CStoreEditor q/:funcs/;
166 use OpenILS::Const qw/:const/;
167 use OpenSRF::EX q/:try/;
168 use OpenILS::Application::AppUtils;
169 use OpenILS::Application::Cat::BibCommon;
170 use OpenILS::Application::Cat::AssetCommon;
171 use MARC::Record;
172 use MARC::Batch;
173 use MARC::File::XML;
174 my $U = 'OpenILS::Application::AppUtils';
175
176
177 # ----------------------------------------------------------------------------
178 # Lineitem
179 # ----------------------------------------------------------------------------
180 sub create_lineitem {
181     my($mgr, %args) = @_;
182     my $li = Fieldmapper::acq::lineitem->new;
183     $li->creator($mgr->editor->requestor->id);
184     $li->selector($li->creator);
185     $li->editor($li->creator);
186     $li->create_time('now');
187     $li->edit_time('now');
188     $li->state('new');
189     $li->$_($args{$_}) for keys %args;
190     if($li->picklist) {
191         return 0 unless update_picklist($mgr, $li->picklist);
192     }
193     $mgr->add_li;
194     return $mgr->editor->create_acq_lineitem($li);
195 }
196
197 sub update_lineitem {
198     my($mgr, $li) = @_;
199     $li->edit_time('now');
200     $li->editor($mgr->editor->requestor->id);
201     return $li if $mgr->editor->update_acq_lineitem($li);
202     $mgr->add_lid;
203     return undef;
204 }
205
206 sub delete_lineitem {
207     my($mgr, $li) = @_;
208     $li = $mgr->editor->retrieve_acq_lineitem($li) unless ref $li;
209
210     if($li->picklist) {
211         return 0 unless update_picklist($mgr, $li->picklist);
212     }
213
214     if($li->purchase_order) {
215         return 0 unless update_purchase_order($mgr, $li->purchase_order);
216     }
217
218     # delete the attached lineitem_details
219     my $lid_ids = $mgr->editor->search_acq_lineitem_detail({lineitem => $li->id}, {idlist=>1});
220     for my $lid_id (@$lid_ids) {
221         return 0 unless delete_lineitem_detail($mgr, undef, $lid_id);
222     }
223
224     return $mgr->editor->delete_acq_lineitem($li);
225 }
226
227 # begins and commit transactions as it goes
228 sub create_lineitem_list_assets {
229     my($mgr, $li_ids) = @_;
230     # create the bibs/volumes/copies and ingest the records
231     for my $li_id (@$li_ids) {
232         $mgr->editor->xact_begin;
233         my $data = create_lineitem_assets($mgr, $li_id) or return undef;
234         $mgr->editor->xact_commit;
235         $mgr->push_ingest_queue($data->{li}->eg_bib_id) if $data->{new_bib};
236         $mgr->respond;
237     }
238     $mgr->process_ingest_records;
239     return 1;
240 }
241
242 # ----------------------------------------------------------------------------
243 # if all of the lineitem details for this lineitem have 
244 # been received, mark the lineitem as received
245 # returns 1 on non-received, li on received, 0 on error
246 # ----------------------------------------------------------------------------
247 sub check_lineitem_received {
248     my($mgr, $li_id) = @_;
249
250     my $non_recv = $mgr->editor->search_acq_lineitem_detail(
251         {recv_time => undef, lineitem => $li_id}, {idlist=>1});
252
253     return 1 unless @$non_recv;
254
255     my $li = $mgr->editor->retrieve_acq_lineitem($li_id);
256     $li->state('received');
257     return update_lineitem($mgr, $li);
258 }
259
260 sub receive_lineitem {
261     my($mgr, $li_id, $skip_complete_check) = @_;
262     my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
263
264     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
265         {lineitem => $li_id, recv_time => undef}, {idlist => 1});
266
267     for my $lid_id (@$lid_ids) {
268        receive_lineitem_detail($mgr, $lid_id, 1) or return 0; 
269     }
270
271     $mgr->add_li;
272     $li->state('received');
273     update_lineitem($mgr, $li) or return 0;
274     return 1 if $skip_complete_check;
275
276     return check_purchase_order_received($mgr, $li->purchase_order);
277 }
278
279 # ----------------------------------------------------------------------------
280 # Lineitem Detail
281 # ----------------------------------------------------------------------------
282 sub create_lineitem_detail {
283     my($mgr, %args) = @_;
284     my $lid = Fieldmapper::acq::lineitem_detail->new;
285     $lid->$_($args{$_}) for keys %args;
286     $mgr->editor->create_acq_lineitem_detail($lid) or return 0;
287     $mgr->add_lid;
288
289     # create some default values
290     unless($lid->barcode) {
291         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_barcode_prefix') || 'ACQ';
292         $lid->barcode($pfx.$lid->id);
293     }
294
295     unless($lid->cn_label) {
296         my $pfx = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.tmp_callnumber_prefix') || 'ACQ';
297         $lid->cn_label($pfx.$lid->id);
298     }
299
300     if(!$lid->location and my $loc = $U->ou_ancestor_setting_value($lid->owning_lib, 'acq.default_copy_location')) {
301         $lid->location($loc);
302     }
303
304     if(!$lid->circ_modifier and my $mod = get_default_circ_modifier($mgr, $lid->owning_lib)) {
305         $lid->circ_modifier($mod);
306     }
307
308     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
309     my $li = $mgr->editor->retrieve_acq_lineitem($lid->lineitem) or return 0;
310     update_lineitem($mgr, $li) or return 0;
311     return $lid;
312 }
313
314 sub get_default_circ_modifier {
315     my($mgr, $org) = @_;
316     my $mod = $mgr->cache($org, 'def_circ_mod');
317     return $mod if $mod;
318     $mod = $U->ou_ancestor_setting_value($org, 'acq.default_circ_modifier');
319     return $mgr->cache($org, 'def_circ_mod', $mod) if $mod;
320     return undef;
321 }
322
323 sub delete_lineitem_detail {
324     my($mgr, $lid) = @_;
325     $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid) unless ref $lid;
326     return $mgr->editor->delete_acq_lineitem_detail($lid);
327 }
328
329
330 sub receive_lineitem_detail {
331     my($mgr, $lid_id, $skip_complete_check) = @_;
332     my $e = $mgr->editor;
333
334     my $lid = $e->retrieve_acq_lineitem_detail([
335         $lid_id,
336         {   flesh => 1,
337             flesh_fields => {
338                 acqlid => ['fund_debit']
339             }
340         }
341     ]) or return 0;
342
343     return 1 if $lid->recv_time;
344
345     $lid->recv_time('now');
346     $e->update_acq_lineitem_detail($lid) or return 0;
347
348     my $copy = $e->retrieve_asset_copy($lid->eg_copy_id) or return 0;
349     $copy->status(OILS_COPY_STATUS_IN_PROCESS);
350     $copy->edit_date('now');
351     $copy->editor($e->requestor->id);
352     $e->update_asset_copy($copy) or return 0;
353
354     if($lid->fund_debit) {
355         $lid->fund_debit->encumbrance('f');
356         $e->update_acq_fund_debit($lid->fund_debit) or return 0;
357     }
358
359     $mgr->add_lid;
360
361     return 1 if $skip_complete_check;
362
363     my $li = check_lineitem_received($mgr, $lid->lineitem) or return 0;
364     return 1 if $li == 1; # li not received
365
366     return check_purchase_order_received($mgr, $li->purchase_order);
367 }
368
369
370 # ----------------------------------------------------------------------------
371 # Lineitem Attr
372 # ----------------------------------------------------------------------------
373 sub set_lineitem_attr {
374     my($mgr, %args) = @_;
375     my $attr_type = $args{attr_type};
376
377     # first, see if it's already set.  May just need to overwrite it
378     my $attr = $mgr->editor->search_acq_lineitem_attr({
379         lineitem => $args{lineitem},
380         attr_type => $args{attr_type},
381         attr_name => $args{attr_name}
382     })->[0];
383
384     if($attr) {
385         $attr->attr_value($args{attr_value});
386         return $attr if $mgr->editor->update_acq_lineitem_attr($attr);
387         return undef;
388
389     } else {
390
391         $attr = Fieldmapper::acq::lineitem_attr->new;
392         $attr->$_($args{$_}) for keys %args;
393         
394         unless($attr->definition) {
395             my $find = "search_acq_$attr_type";
396             my $attr_def_id = $mgr->editor->$find({code => $attr->attr_name}, {idlist=>1})->[0] or return 0;
397             $attr->definition($attr_def_id);
398         }
399         return $mgr->editor->create_acq_lineitem_attr($attr);
400     }
401 }
402
403 sub get_li_price {
404     my $li = shift;
405     my $attrs = $li->attributes;
406     my ($marc_estimated, $local_estimated, $local_actual, $prov_estimated, $prov_actual);
407
408     for my $attr (@$attrs) {
409         if($attr->attr_name eq 'estimated_price') {
410             $local_estimated = $attr->attr_value 
411                 if $attr->attr_type eq 'lineitem_local_attr_definition';
412             $prov_estimated = $attr->attr_value 
413                 if $attr->attr_type eq 'lineitem_prov_attr_definition';
414             $marc_estimated = $attr->attr_value
415                 if $attr->attr_type eq 'lineitem_marc_attr_definition';
416
417         } elsif($attr->attr_name eq 'actual_price') {
418             $local_actual = $attr->attr_value     
419                 if $attr->attr_type eq 'lineitem_local_attr_definition';
420             $prov_actual = $attr->attr_value 
421                 if $attr->attr_type eq 'lineitem_prov_attr_definition';
422         }
423     }
424
425     return ($local_actual, 1) if $local_actual;
426     return ($prov_actual, 2) if $prov_actual;
427     return ($local_estimated, 1) if $local_estimated;
428     return ($prov_estimated, 2) if $prov_estimated;
429     return ($marc_estimated, 3);
430 }
431
432
433 # ----------------------------------------------------------------------------
434 # Lineitem Debits
435 # ----------------------------------------------------------------------------
436 sub create_lineitem_debits {
437     my($mgr, $li, $price, $ptype) = @_; 
438
439     ($price, $ptype) = get_li_price($li) unless $price;
440
441     unless($price) {
442         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PRICE', payload => $li->id));
443         $mgr->editor->rollback;
444         return 0;
445     }
446
447     unless($li->provider) {
448         $mgr->editor->event(OpenILS::Event->new('ACQ_LINEITEM_NO_PROVIDER', payload => $li->id));
449         $mgr->editor->rollback;
450         return 0;
451     }
452
453     my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
454         {lineitem => $li->id}, 
455         {idlist=>1}
456     );
457
458     for my $lid_id (@$lid_ids) {
459
460         my $lid = $mgr->editor->retrieve_acq_lineitem_detail([
461             $lid_id,
462             {   flesh => 1, 
463                 flesh_fields => {acqlid => ['fund']}
464             }
465         ]);
466
467         create_lineitem_detail_debit($mgr, $li, $lid, $price, $ptype) or return 0;
468     }
469
470     return 1;
471 }
472
473
474 # flesh li->provider
475 # flesh lid->fund
476 # ptype 1=local, 2=provider, 3=marc
477 sub create_lineitem_detail_debit {
478     my($mgr, $li, $lid, $price, $ptype) = @_;
479
480     unless(ref $li and ref $li->provider) {
481        $li = $mgr->editor->retrieve_acq_lineitem([
482             $li,
483             {   flesh => 1,
484                 flesh_fields => {jub => ['provider']},
485             }
486         ]);
487     }
488
489     unless(ref $lid and ref $lid->fund) {
490         $lid = $mgr->editor->retrieve_acq_lineitem_detail([
491             $lid,
492             {   flesh => 1, 
493                 flesh_fields => {acqlid => ['fund']}
494             }
495         ]);
496     }
497
498     my $ctype = $lid->fund->currency_type;
499     my $amount = $price;
500
501     if($ptype == 2) { # price from vendor
502         $ctype = $li->provider->currency_type;
503         $amount = currency_conversion($mgr, $ctype, $lid->fund->currency_type, $price);
504     }
505
506     my $debit = create_fund_debit(
507         $mgr, 
508         fund => $lid->fund->id,
509         origin_amount => $price,
510         origin_currency_type => $ctype,
511         amount => $amount
512     ) or return 0;
513
514     $lid->fund_debit($debit->id);
515     $lid->fund($lid->fund->id);
516     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
517     return $debit;
518 }
519
520
521 # ----------------------------------------------------------------------------
522 # Fund Debit
523 # ----------------------------------------------------------------------------
524 sub create_fund_debit {
525     my($mgr, %args) = @_;
526     my $debit = Fieldmapper::acq::fund_debit->new;
527     $debit->debit_type('purchase');
528     $debit->encumbrance('t');
529     $debit->$_($args{$_}) for keys %args;
530     $mgr->add_debit($debit->amount);
531     return $mgr->editor->create_acq_fund_debit($debit);
532 }
533
534 sub currency_conversion {
535     my($mgr, $src_currency, $dest_currency, $amount) = @_;
536     my $result = $mgr->editor->json_query(
537         {from => ['acq.exchange_ratio', $src_currency, $dest_currency, $amount]});
538     return $result->[0]->{'acq.exchange_ratio'};
539 }
540
541
542 # ----------------------------------------------------------------------------
543 # Picklist
544 # ----------------------------------------------------------------------------
545 sub create_picklist {
546     my($mgr, %args) = @_;
547     my $picklist = Fieldmapper::acq::picklist->new;
548     $picklist->creator($mgr->editor->requestor->id);
549     $picklist->owner($picklist->creator);
550     $picklist->editor($picklist->creator);
551     $picklist->create_time('now');
552     $picklist->edit_time('now');
553     $picklist->org_unit($mgr->editor->requestor->ws_ou);
554     $picklist->owner($mgr->editor->requestor->id);
555     $picklist->$_($args{$_}) for keys %args;
556     $mgr->picklist($picklist);
557     return $mgr->editor->create_acq_picklist($picklist);
558 }
559
560 sub update_picklist {
561     my($mgr, $picklist) = @_;
562     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
563     $picklist->edit_time('now');
564     $picklist->editor($mgr->editor->requestor->id);
565     $mgr->picklist($picklist);
566     return $picklist if $mgr->editor->update_acq_picklist($picklist);
567     return undef;
568 }
569
570 sub delete_picklist {
571     my($mgr, $picklist) = @_;
572     $picklist = $mgr->editor->retrieve_acq_picklist($picklist) unless ref $picklist;
573
574     # delete all 'new' lineitems
575     my $lis = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => 'new'});
576     for my $li (@$lis) {
577         return 0 unless delete_lineitem($mgr, $li);
578     }
579
580     # detach all non-'new' lineitems
581     $lis = $mgr->editor->search_acq_lineitem({picklist => $picklist->id, state => {'!=' => 'new'}});
582     for my $li (@$lis) {
583         $li->clear_picklist;
584         return 0 unless update_lineitem($li);
585     }
586
587     # remove any picklist-specific object perms
588     my $ops = $mgr->editor->search_permission_usr_object_perm_map({object_type => 'acqpl', object_id => ''.$picklist->id});
589     for my $op (@$ops) {
590         return 0 unless $mgr->editor->delete_usr_object_perm_map($op);
591     }
592
593     return $mgr->editor->delete_acq_picklist($picklist);
594 }
595
596 # ----------------------------------------------------------------------------
597 # Purchase Order
598 # ----------------------------------------------------------------------------
599 sub update_purchase_order {
600     my($mgr, $po) = @_;
601     $po = $mgr->editor->retrieve_acq_purchase_order($po) unless ref $po;
602     $po->editor($mgr->editor->requestor->id);
603     $po->edit_time('now');
604     $mgr->purchase_order($po);
605     return $po if $mgr->editor->update_acq_purchase_order($po);
606     return undef;
607 }
608
609 sub create_purchase_order {
610     my($mgr, %args) = @_;
611     my $po = Fieldmapper::acq::purchase_order->new;
612     $po->creator($mgr->editor->requestor->id);
613     $po->editor($mgr->editor->requestor->id);
614     $po->owner($mgr->editor->requestor->id);
615     $po->edit_time('now');
616     $po->create_time('now');
617     $po->ordering_agency($mgr->editor->requestor->ws_ou);
618     $po->$_($args{$_}) for keys %args;
619     $mgr->purchase_order($po);
620     return $mgr->editor->create_acq_purchase_order($po);
621 }
622
623 # ----------------------------------------------------------------------------
624 # if all of the lineitems for this PO are received,
625 # mark the PO as received
626 # ----------------------------------------------------------------------------
627 sub check_purchase_order_received {
628     my($mgr, $po_id) = @_;
629
630     my $non_recv_li = $mgr->editor->search_acq_lineitem(
631         {   purchase_order => $po_id,
632             state => {'!=' => 'received'}
633         }, {idlist=>1});
634
635     return 1 if @$non_recv_li;
636
637     my $po = $mgr->editor->retrieve_acq_purchase_order($po_id);
638     $po->state('received');
639     return update_purchase_order($mgr, $po);
640 }
641
642
643 # ----------------------------------------------------------------------------
644 # Bib, Callnumber, and Copy data
645 # ----------------------------------------------------------------------------
646
647 sub create_lineitem_assets {
648     my($mgr, $li_id) = @_;
649     my $evt;
650
651     my $li = $mgr->editor->retrieve_acq_lineitem([
652         $li_id,
653         {   flesh => 1,
654             flesh_fields => {jub => ['purchase_order', 'attributes']}
655         }
656     ]) or return 0;
657
658     # -----------------------------------------------------------------
659     # first, create the bib record if necessary
660     # -----------------------------------------------------------------
661     my $new_bib = 0;
662     unless($li->eg_bib_id) {
663         create_bib($mgr, $li) or return 0;
664         $new_bib = 1;
665     }
666
667     my $li_details = $mgr->editor->search_acq_lineitem_detail({lineitem => $li_id}, {idlist=>1});
668
669     # -----------------------------------------------------------------
670     # for each lineitem_detail, create the volume if necessary, create 
671     # a copy, and link them all together.
672     # -----------------------------------------------------------------
673     for my $lid_id (@{$li_details}) {
674
675         my $lid = $mgr->editor->retrieve_acq_lineitem_detail($lid_id) or return 0;
676         next if $lid->eg_copy_id;
677
678         my $org = $lid->owning_lib;
679         my $label = $lid->cn_label;
680         my $bibid = $li->eg_bib_id;
681
682         my $volume = $mgr->cache($org, "cn.$bibid.$label");
683         unless($volume) {
684             $volume = create_volume($mgr, $li, $lid) or return 0;
685             $mgr->cache($org, "cn.$bibid.$label", $volume);
686         }
687         create_copy($mgr, $volume, $lid) or return 0;
688     }
689
690     return { li => $li, new_bib => $new_bib };
691 }
692
693 sub create_bib {
694     my($mgr, $li) = @_;
695
696     my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
697         $mgr->editor, 
698         $li->marc, 
699         undef, 
700         undef, 
701         1, # override tcn collisions
702         1, # no-ingest
703         undef # $rec->bib_source
704     ); 
705
706     if($U->event_code($record)) {
707         $mgr->editor->event($record);
708         $mgr->editor->rollback;
709         return 0;
710     }
711
712     $li->eg_bib_id($record->id);
713     $mgr->add_bib;
714     return update_lineitem($mgr, $li);
715 }
716
717 sub create_volume {
718     my($mgr, $li, $lid) = @_;
719
720     my ($volume, $evt) = 
721         OpenILS::Application::Cat::AssetCommon->find_or_create_volume(
722             $mgr->editor, 
723             $lid->cn_label, 
724             $li->eg_bib_id, 
725             $lid->owning_lib
726         );
727
728     if($evt) {
729         $mgr->editor->event($evt);
730         return 0;
731     }
732
733     return $volume;
734 }
735
736 sub create_copy {
737     my($mgr, $volume, $lid) = @_;
738     my $copy = Fieldmapper::asset::copy->new;
739     $copy->isnew(1);
740     $copy->loan_duration(2);
741     $copy->fine_level(2);
742     $copy->status(OILS_COPY_STATUS_ON_ORDER);
743     $copy->barcode($lid->barcode);
744     $copy->location($lid->location);
745     $copy->call_number($volume->id);
746     $copy->circ_lib($volume->owning_lib);
747     $copy->circ_modifier($lid->circ_modifier);
748
749     my $evt = OpenILS::Application::Cat::AssetCommon->create_copy($mgr->editor, $volume, $copy);
750     if($evt) {
751         $mgr->editor->event($evt);
752         return 0;
753     }
754
755     $mgr->add_copy;
756     $lid->eg_copy_id($copy->id);
757     $mgr->editor->update_acq_lineitem_detail($lid) or return 0;
758 }
759
760
761
762
763
764
765 # ----------------------------------------------------------------------------
766 # Workflow: Build a selection list from a Z39.50 search
767 # ----------------------------------------------------------------------------
768
769 __PACKAGE__->register_method(
770         method => 'zsearch',
771         api_name => 'open-ils.acq.picklist.search.z3950',
772     stream => 1,
773         signature => {
774         desc => 'Performs a z3950 federated search and creates a picklist and associated lineitems',
775         params => [
776             {desc => 'Authentication token', type => 'string'},
777             {desc => 'Search definition', type => 'object'},
778             {desc => 'Picklist name, optional', type => 'string'},
779         ]
780     }
781 );
782
783 sub zsearch {
784     my($self, $conn, $auth, $search, $name, $options) = @_;
785     my $e = new_editor(authtoken=>$auth);
786     return $e->event unless $e->checkauth;
787     return $e->event unless $e->allowed('CREATE_PICKLIST');
788
789     $search->{limit} ||= 10;
790     $options ||= {};
791
792     my $ses = OpenSRF::AppSession->create('open-ils.search');
793     my $req = $ses->request('open-ils.search.z3950.search_class', $auth, $search);
794
795     my $first = 1;
796     my $picklist;
797     my $mgr;
798     while(my $resp = $req->recv(timeout=>60)) {
799
800         if($first) {
801             my $e = new_editor(requestor=>$e->requestor, xact=>1);
802             $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
803             $picklist = zsearch_build_pl($mgr, $name);
804             $first = 0;
805         }
806
807         my $result = $resp->content;
808         my $count = $result->{count};
809         $mgr->total( (($count < $search->{limit}) ? $count : $search->{limit})+1 );
810
811         for my $rec (@{$result->{records}}) {
812
813             my $li = create_lineitem($mgr, 
814                 picklist => $picklist->id,
815                 source_label => $result->{service},
816                 marc => $rec->{marcxml},
817                 eg_bib_id => $rec->{bibid}
818             );
819
820             if($$options{respond_li}) {
821                 $li->attributes($mgr->editor->search_acq_lineitem_attr({lineitem => $li->id}))
822                     if $$options{flesh_attrs};
823                 $li->clear_marc if $$options{clear_marc};
824                 $mgr->respond(lineitem => $li);
825             } else {
826                 $mgr->respond;
827             }
828         }
829     }
830
831     $mgr->editor->commit;
832     return $mgr->respond_complete;
833 }
834
835 sub zsearch_build_pl {
836     my($mgr, $name) = @_;
837     $name ||= '';
838
839     my $picklist = $mgr->editor->search_acq_picklist({
840         owner => $mgr->editor->requestor->id, 
841         name => $name
842     })->[0];
843
844     if($name eq '' and $picklist) {
845         return 0 unless delete_picklist($mgr, $picklist);
846         $picklist = undef;
847     }
848
849     return update_picklist($mgr, $picklist) if $picklist;
850     return create_picklist($mgr, name => $name);
851 }
852
853
854 # ----------------------------------------------------------------------------
855 # Workflow: Build a selection list / PO by importing a batch of MARC records
856 # ----------------------------------------------------------------------------
857
858 __PACKAGE__->register_method(
859     method => 'upload_records',
860     api_name => 'open-ils.acq.process_upload_records',
861     stream => 1,
862 );
863
864 sub upload_records {
865     my($self, $conn, $auth, $key) = @_;
866
867         my $e = new_editor(authtoken => $auth, xact => 1);
868     return $e->die_event unless $e->checkauth;
869     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
870
871     my $cache = OpenSRF::Utils::Cache->new;
872
873     my $data = $cache->get_cache("vandelay_import_spool_$key");
874         my $purpose = $data->{purpose};
875     my $filename = $data->{path};
876     my $provider = $data->{provider};
877     my $picklist = $data->{picklist};
878     my $create_po = $data->{create_po};
879     my $ordering_agency = $data->{ordering_agency};
880     my $create_assets = $data->{create_assets};
881     my $po;
882     my $evt;
883
884     unless(-r $filename) {
885         $logger->error("unable to read MARC file $filename");
886         $e->rollback;
887         return OpenILS::Event->new('FILE_UPLOAD_ERROR', payload => {filename => $filename});
888     }
889
890     $provider = $e->retrieve_acq_provider($provider) or return $e->die_event;
891
892     if($picklist) {
893         $picklist = $e->retrieve_acq_picklist($picklist) or return $e->die_event;
894         if($picklist->owner != $e->requestor->id) {
895             return $e->die_event unless 
896                 $e->allowed('CREATE_PICKLIST', $picklist->org_unit, $picklist);
897         }
898     }
899
900     if($create_po) {
901         $po = create_purchase_order($mgr, 
902             ordering_agency => $ordering_agency,
903             provider => $provider->id
904         ) or return $mgr->editor->die_event;
905     }
906
907     $logger->info("acq processing MARC file=$filename");
908
909     my $marctype = 'USMARC'; # ?
910         my $batch = new MARC::Batch ($marctype, $filename);
911         $batch->strict_off;
912
913         my $count = 0;
914     my @li_list;
915
916         while(1) {
917
918             my $err;
919         my $xml;
920                 $count++;
921         my $r;
922
923                 try {
924             $r = $batch->next;
925         } catch Error with {
926             $err = shift;
927                         $logger->warn("Proccessing of record $count in set $key failed with error $err.  Skipping this record");
928         };
929
930         next if $err;
931         last unless $r;
932
933                 try {
934             ($xml = $r->as_xml_record()) =~ s/\n//sog;
935             $xml =~ s/^<\?xml.+\?\s*>//go;
936             $xml =~ s/>\s+</></go;
937             $xml =~ s/\p{Cc}//go;
938             $xml = $U->entityize($xml);
939             $xml =~ s/[\x00-\x1f]//go;
940
941                 } catch Error with {
942                         $err = shift;
943                         $logger->warn("Proccessing XML of record $count in set $key failed with error $err.  Skipping this record");
944                 };
945
946         next if $err or not $xml;
947
948         my %args = (
949             source_label => $provider->code,
950             provider => $provider->id,
951             marc => $xml,
952         );
953
954         $args{picklist} = $picklist->id if $picklist;
955         if($po) {
956             $args{purchase_order} = $po->id;
957             $args{state} = 'on-order';
958         }
959
960         my $li = create_lineitem($mgr, %args) or return $mgr->editor->die_event;
961         $mgr->respond;
962         $li->provider($provider); # flesh it, we'll need it later
963
964         import_lineitem_details($mgr, $ordering_agency, $li) or return $mgr->editor->die_event;
965         $mgr->respond;
966
967         push(@li_list, $li->id);
968         $mgr->respond;
969         }
970
971         $e->commit;
972     unlink($filename);
973     $cache->delete_cache('vandelay_import_spool_' . $key);
974
975     if($create_assets) {
976         create_lineitem_list_assets($mgr, \@li_list) or return $e->die_event;
977     }
978
979     return $mgr->respond_complete;
980 }
981
982 sub import_lineitem_details {
983     my($mgr, $ordering_agency, $li) = @_;
984
985     my $holdings = $mgr->editor->json_query({from => ['acq.extract_provider_holding_data', $li->id]});
986     return 1 unless @$holdings;
987     my $org_path = $U->get_org_ancestors($ordering_agency);
988     $org_path = [ reverse (@$org_path) ];
989     my $price;
990
991     my $idx = 1;
992     while(1) {
993         # create a lineitem detail for each copy in the data
994
995         my $compiled = extract_lineitem_detail_data($mgr, $org_path, $holdings, $idx);
996         last unless defined $compiled;
997         return 0 unless $compiled;
998
999         # this takes the price of the last copy and uses it as the lineitem price
1000         # need to determine if a given record would include different prices for the same item
1001         $price = $$compiled{price};
1002
1003         for(1..$$compiled{quantity}) {
1004             my $lid = create_lineitem_detail($mgr, 
1005                 lineitem => $li->id,
1006                 owning_lib => $$compiled{owning_lib},
1007                 cn_label => $$compiled{call_number},
1008                 fund => $$compiled{fund},
1009                 circ_modifier => $$compiled{circ_modifier},
1010                 note => $$compiled{note},
1011                 location => $$compiled{copy_location}
1012             ) or return 0;
1013         }
1014
1015         $mgr->respond;
1016         $idx++;
1017     }
1018
1019     # set the price attr so we'll know the source of the price
1020     set_lineitem_attr(
1021         $mgr, 
1022         attr_name => 'estimated_price',
1023         attr_type => 'lineitem_local_attr_definition',
1024         attr_value => $price,
1025         lineitem => $li->id
1026     ) or return 0;
1027
1028     # if we're creating a purchase order, create the debits
1029     if($li->purchase_order) {
1030         create_lineitem_debits($mgr, $li, $price, 2) or return 0;
1031         $mgr->respond;
1032     }
1033
1034     return 1;
1035 }
1036
1037 # return hash on success, 0 on error, undef on no more holdings
1038 sub extract_lineitem_detail_data {
1039     my($mgr, $org_path, $holdings, $index) = @_;
1040
1041     my @data_list = grep { $_->{holding} eq $index } @$holdings;
1042     return undef unless @data_list;
1043
1044     my %compiled = map { $_->{attr} => $_->{data} } @data_list;
1045     my $base_org = $$org_path[0];
1046
1047     my $killme = sub {
1048         my $msg = shift;
1049         $logger->error("Item import extraction error: $msg");
1050         $logger->error('Holdings Data: ' . OpenSRF::Utils::JSON->perl2JSON(\%compiled));
1051         $mgr->editor->rollback;
1052         $mgr->editor->event(OpenILS::Event->new('ACQ_IMPORT_ERROR', payload => $msg));
1053         return 0;
1054     };
1055
1056     $compiled{quantity} ||= 1;
1057
1058     # ---------------------------------------------------------------------
1059     # Fund
1060     my $code = $compiled{fund_code};
1061     return $killme->('no fund code provided') unless $code;
1062
1063     my $fund = $mgr->cache($base_org, "fund.$code");
1064     unless($fund) {
1065         # search up the org tree for the most appropriate fund
1066         for my $org (@$org_path) {
1067             $fund = $mgr->editor->search_acq_fund(
1068                 {org => $org, code => $code, year => DateTime->now->year}, {idlist => 1})->[0];
1069             last if $fund;
1070         }
1071     }
1072     return $killme->("no fund with code $code at orgs [@$org_path]") unless $fund;
1073     $compiled{fund} = $fund;
1074     $mgr->cache($base_org, "fund.$code", $fund);
1075
1076
1077     # ---------------------------------------------------------------------
1078     # Owning lib
1079     my $sn = $compiled{owning_lib};
1080     return $killme->('no owning_lib defined') unless $sn;
1081     my $org_id = 
1082         $mgr->cache($base_org, "orgsn.$sn") ||
1083             $mgr->editor->search_actor_org_unit({shortname => $sn}, {idlist => 1})->[0];
1084     return $killme->("invalid owning_lib defined: $sn") unless $org_id;
1085     $compiled{owning_lib} = $org_id;
1086     $mgr->cache($$org_path[0], "orgsn.$sn", $org_id);
1087
1088
1089     # ---------------------------------------------------------------------
1090     # Circ Modifier
1091     my $mod;
1092     $code = $compiled{circ_modifier};
1093
1094     if($code) {
1095
1096         $mod = $mgr->cache($base_org, "mod.$code") ||
1097             $mgr->editor->retrieve_config_circ_modifier($code);
1098         return $killme->("invlalid circ_modifier $code") unless $mod;
1099         $mgr->cache($base_org, "mod.$code", $mod);
1100
1101     } else {
1102         # try the default
1103         $mod = get_default_circ_modifier($mgr, $base_org)
1104             or return $killme->('no circ_modifier defined');
1105     }
1106
1107     $compiled{circ_modifier} = $mod;
1108
1109
1110     # ---------------------------------------------------------------------
1111     # Shelving Location
1112     my $name = $compiled{copy_location};
1113     return $killme->('no copy_location defined') unless $name;
1114     my $loc = $mgr->cache($base_org, "copy_loc.$name");
1115     unless($loc) {
1116         for my $org (@$org_path) {
1117             $loc = $mgr->editor->search_asset_copy_location(
1118                 {owning_lib => $org, name => $name}, {idlist => 1})->[0];
1119             last if $loc;
1120         }
1121     }
1122     return $killme->("Invalid copy location $name") unless $loc;
1123     $compiled{copy_location} = $loc;
1124     $mgr->cache($base_org, "copy_loc.$name", $loc);
1125
1126     return \%compiled;
1127 }
1128
1129
1130
1131 # ----------------------------------------------------------------------------
1132 # Workflow: Given an existing purchase order, import/create the bibs, 
1133 # callnumber and copy objects
1134 # ----------------------------------------------------------------------------
1135
1136 __PACKAGE__->register_method(
1137         method => 'create_po_assets',
1138         api_name        => 'open-ils.acq.purchase_order.assets.create',
1139         signature => {
1140         desc => q/Creates assets for each lineitem in the purchase order/,
1141         params => [
1142             {desc => 'Authentication token', type => 'string'},
1143             {desc => 'The purchase order id', type => 'number'},
1144         ],
1145         return => {desc => 'Streams a total versus completed counts object, event on error'}
1146     }
1147 );
1148
1149 sub create_po_assets {
1150     my($self, $conn, $auth, $po_id) = @_;
1151
1152     my $e = new_editor(authtoken=>$auth, xact=>1);
1153     return $e->die_event unless $e->checkauth;
1154     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1155
1156     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1157     return $e->die_event unless $e->allowed('IMPORT_PURCHASE_ORDER_ASSETS', $po->ordering_agency);
1158
1159     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1160
1161     # it's ugly, but it's fast.  Get the total count of lineitem detail objects to process
1162     my $lid_total = $e->json_query({
1163         select => { acqlid => [{aggregate => 1, transform => 'count', column => 'id'}] }, 
1164         from => {
1165             acqlid => {
1166                 jub => {
1167                     fkey => 'lineitem', 
1168                     field => 'id', 
1169                     join => {acqpo => {fkey => 'purchase_order', field => 'id'}}
1170                 }
1171             }
1172         }, 
1173         where => {'+acqpo' => {id => $po_id}}
1174     })->[0]->{id};
1175
1176     $mgr->total(scalar(@$li_ids) + $lid_total);
1177
1178     create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1179
1180     $e->xact_begin;
1181     update_purchase_order($mgr, $po) or return $e->die_event;
1182     $e->commit;
1183
1184     return $mgr->respond_complete;
1185 }
1186
1187
1188
1189 __PACKAGE__->register_method(
1190         method => 'create_purchase_order_api',
1191         api_name        => 'open-ils.acq.purchase_order.create',
1192         signature => {
1193         desc => 'Creates a new purchase order',
1194         params => [
1195             {desc => 'Authentication token', type => 'string'},
1196             {desc => 'purchase_order to create', type => 'object'}
1197         ],
1198         return => {desc => 'The purchase order id, Event on failure'}
1199     }
1200 );
1201
1202 sub create_purchase_order_api {
1203     my($self, $conn, $auth, $po, $args) = @_;
1204     $args ||= {};
1205
1206     my $e = new_editor(xact=>1, authtoken=>$auth);
1207     return $e->die_event unless $e->checkauth;
1208     return $e->die_event unless $e->allowed('CREATE_PURCHASE_ORDER', $po->ordering_agency);
1209     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1210
1211     # create the PO
1212     my %pargs = (ordering_agency => $e->requestor->ws_ou);
1213     $pargs{provider} = $po->provider if $po->provider;
1214     $po = create_purchase_order($mgr, %pargs) or return $e->die_event;
1215
1216     my $li_ids = $$args{lineitems};
1217
1218     if($li_ids) {
1219
1220         for my $li_id (@$li_ids) { 
1221
1222             my $li = $e->retrieve_acq_lineitem([
1223                 $li_id,
1224                 {flesh => 1, flesh_fields => {jub => ['attributes']}}
1225             ]) or return $e->die_event;
1226
1227             $li->provider($po->provider);
1228             $li->purchase_order($po->id);
1229             update_lineitem($mgr, $li) or return $e->die_event;
1230             $mgr->respond;
1231
1232             create_lineitem_debits($mgr, $li) or return $e->die_event;
1233         }
1234     }
1235
1236     # commit before starting the asset creation
1237     $e->xact_commit;
1238
1239     if($li_ids and $$args{create_assets}) {
1240         create_lineitem_list_assets($mgr, $li_ids) or return $e->die_event;
1241     }
1242
1243     return $mgr->respond_complete;
1244 }
1245
1246
1247 __PACKAGE__->register_method(
1248         method => 'lineitem_detail_CUD_batch',
1249         api_name => 'open-ils.acq.lineitem_detail.cud.batch',
1250     stream => 1,
1251         signature => {
1252         desc => q/Creates a new purchase order line item detail.  
1253             Additionally creates the associated fund_debit/,
1254         params => [
1255             {desc => 'Authentication token', type => 'string'},
1256             {desc => 'List of lineitem_details to create', type => 'array'},
1257         ],
1258         return => {desc => 'Streaming response of current position in the array'}
1259     }
1260 );
1261
1262 sub lineitem_detail_CUD_batch {
1263     my($self, $conn, $auth, $li_details) = @_;
1264
1265     my $e = new_editor(xact=>1, authtoken=>$auth);
1266     return $e->die_event unless $e->checkauth;
1267     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1268
1269     # XXX perms
1270
1271     $mgr->total(scalar(@$li_details));
1272     
1273     my %li_cache;
1274
1275     for my $lid (@$li_details) {
1276
1277         my $li = $li_cache{$lid->lineitem} || $e->retrieve_acq_lineitem($lid->lineitem);
1278
1279         if($lid->isnew) {
1280             create_lineitem_detail($mgr, %{$lid->to_bare_hash}) or return $e->die_event;
1281
1282         } elsif($lid->ischanged) {
1283             $e->update_acq_lineitem_detail($lid) or return $e->die_event;
1284
1285         } elsif($lid->isdeleted) {
1286             delete_lineitem_detail($mgr, $lid) or return $e->die_event;
1287         }
1288
1289         $mgr->respond(li => $li);
1290         $li_cache{$lid->lineitem} = $li;
1291     }
1292
1293     $e->commit;
1294     return $mgr->respond_complete;
1295 }
1296
1297
1298 __PACKAGE__->register_method(
1299         method => 'receive_po',
1300         api_name        => 'open-ils.acq.purchase_order.receive'
1301 );
1302
1303 sub receive_po {
1304     my($self, $conn, $auth, $po_id) = @_;
1305     my $e = new_editor(xact => 1, authtoken => $auth);
1306     return $e->die_event unless $e->checkauth;
1307     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1308
1309     my $po = $e->retrieve_acq_purchase_order($po_id) or return $e->die_event;
1310     return $e->die_event unless $e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
1311
1312     my $li_ids = $e->search_acq_lineitem({purchase_order => $po_id}, {idlist => 1});
1313
1314     for my $li_id (@$li_ids) {
1315         receive_lineitem($mgr, $li_id) or return $e->die_event;
1316         $mgr->respond;
1317     }
1318
1319     $po->state('received');
1320     update_purchase_order($mgr, $po) or return $e->die_event;
1321
1322     $e->commit;
1323     return $mgr->respond_complete;
1324 }
1325
1326
1327 __PACKAGE__->register_method(
1328         method => 'receive_lineitem_detail_api',
1329         api_name        => 'open-ils.acq.lineitem_detail.receive',
1330         signature => {
1331         desc => 'Mark a lineitem_detail as received',
1332         params => [
1333             {desc => 'Authentication token', type => 'string'},
1334             {desc => 'lineitem detail ID', type => 'number'}
1335         ],
1336         return => {desc => '1 on success, Event on error'}
1337     }
1338 );
1339
1340 sub receive_lineitem_detail_api {
1341     my($self, $conn, $auth, $lid_id) = @_;
1342
1343     my $e = new_editor(xact=>1, authtoken=>$auth);
1344     return $e->die_event unless $e->checkauth;
1345     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1346
1347     my $lid = $e->retrieve_acq_lineitem_detail([
1348         $lid_id, {
1349             flesh => 2,
1350             flesh_fields => {
1351                 acqlid => ['lineitem'],
1352                 jub => ['purchase_order']
1353             }
1354         }
1355     ]);
1356
1357     return $e->die_event unless $e->allowed(
1358         'RECEIVE_PURCHASE_ORDER', $lid->lineitem->purchase_order->ordering_agency);
1359
1360     receive_lineitem_detail($mgr, $lid_id) or return $e->die_event;
1361     $e->commit;
1362     return 1;
1363 }
1364
1365 __PACKAGE__->register_method(
1366         method => 'receive_lineitem_api',
1367         api_name        => 'open-ils.acq.lineitem.receive',
1368         signature => {
1369         desc => 'Mark a lineitem as received',
1370         params => [
1371             {desc => 'Authentication token', type => 'string'},
1372             {desc => 'lineitem detail ID', type => 'number'}
1373         ],
1374         return => {desc => '1 on success, Event on error'}
1375     }
1376 );
1377
1378 sub receive_lineitem_api {
1379     my($self, $conn, $auth, $li_id) = @_;
1380
1381     my $e = new_editor(xact=>1, authtoken=>$auth);
1382     return $e->die_event unless $e->checkauth;
1383     my $mgr = OpenILS::Application::Acq::BatchManager->new(editor => $e, conn => $conn);
1384
1385     my $li = $e->retrieve_acq_lineitem_detail([
1386         $li_id, {
1387             flesh => 1,
1388             flesh_fields => {
1389                 jub => ['purchase_order']
1390             }
1391         }
1392     ]);
1393
1394     return $e->die_event unless $e->allowed(
1395         'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency);
1396
1397     receive_lineitem($mgr, $li_id) or return $e->die_event;
1398     $e->commit;
1399     return 1;
1400 }
1401
1402
1403
1404 1;