1 import {Injectable, EventEmitter} from '@angular/core';
2 import {Observable, from, concat, empty} from 'rxjs';
3 import {switchMap, map, tap, merge} from 'rxjs/operators';
4 import {IdlObject, IdlService} from '@eg/core/idl.service';
5 import {NetService} from '@eg/core/net.service';
6 import {AuthService} from '@eg/core/auth.service';
7 import {PcrudService} from '@eg/core/pcrud.service';
8 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
9 import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
11 export interface BatchLineitemStruct {
14 existing_copies: number;
15 all_locations: IdlObject[];
16 all_funds: IdlObject[];
17 all_circ_modifiers: IdlObject[];
20 export interface BatchLineitemUpdateStruct {
25 complete: number; // Perl bool
27 [key: string]: any; // Perl Acq::BatchManager
30 interface FleshedLineitemParams {
32 // Flesh data beyond the default.
35 // OK to pull the requested LI from the cache.
38 // OK to add this LI to the cache.
39 // Generally a good thing, but if you are fetching an LI with
40 // fewer fleshed fields than the default, this could break code.
45 export class LineitemService {
47 liAttrDefs: IdlObject[];
49 // Emitted when our copy batch editor wants to apply a value
50 // to a set of inputs. This allows the the copy input comboboxes, etc.
51 // to add the entry before it's forced to grab the value from the
52 // server, often in large parallel batches.
53 batchOptionWanted: EventEmitter<{[field: string]: ComboboxEntry}>
54 = new EventEmitter<{[field: string]: ComboboxEntry}> ();
56 // Emits a LI ID if the LI was edited in a way that could impact
57 // its activatability of its linked PO.
58 activateStateChange: EventEmitter<number> = new EventEmitter<number>();
60 // Cache for circ modifiers and funds; locations are cached in the
61 // item location select service.
62 circModCache: {[code: string]: IdlObject} = {};
63 fundCache: {[id: number]: IdlObject} = {};
64 liCache: {[id: number]: BatchLineitemStruct} = {};
66 // Alerts the user has already confirmed are OK.
67 alertAcks: {[id: number]: boolean} = {};
70 private idl: IdlService,
71 private net: NetService,
72 private auth: AuthService,
73 private pcrud: PcrudService,
74 private loc: ItemLocationService
77 getFleshedLineitems(ids: number[],
78 params: FleshedLineitemParams = {}): Observable<BatchLineitemStruct> {
80 if (params.fromCache) {
81 const fromCache = this.getLineitemsFromCache(ids);
82 if (fromCache) { return from(fromCache); }
85 const flesh: any = Object.assign({
88 flesh_order_summary: true,
89 flesh_cancel_reason: true,
90 flesh_li_details: true,
93 flesh_circ_modifier: true,
95 flesh_fund_debit: true,
101 }, params.fleshMore || {});
103 return this.net.request(
104 'open-ils.acq', 'open-ils.acq.lineitem.retrieve.batch',
105 this.auth.token(), ids, flesh
106 ).pipe(tap(liStruct =>
107 this.ingestLineitem(liStruct, params.toCache)));
110 getLineitemsFromCache(ids: number[]): BatchLineitemStruct[] {
112 const fromCache: BatchLineitemStruct[] = [];
115 if (this.liCache[id]) { fromCache.push(this.liCache[id]); }
118 // Only return LI's from cache if all of the requested LI's
119 // are cached, otherwise they would be returned in the wrong
120 // order. Typically it will be all or none so I'm not
121 // fussing with interleaving cached and uncached lineitems
122 // to fix the sorting.
123 if (fromCache.length === ids.length) { return fromCache; }
129 liStruct: BatchLineitemStruct, toCache: boolean): BatchLineitemStruct {
131 const li = liStruct.lineitem;
133 // These values come through as NULL
134 const summary = li.order_summary();
135 if (!summary.estimated_amount()) { summary.estimated_amount(0); }
136 if (!summary.encumbrance_amount()) { summary.encumbrance_amount(0); }
137 if (!summary.paid_amount()) { summary.paid_amount(0); }
139 // Sort the formula applications
140 li.distribution_formulas(
141 li.distribution_formulas().sort((f1, f2) =>
142 f1.create_time() < f2.create_time() ? -1 : 1)
145 // consistent sorting
147 li.lineitem_details().sort((d1, d2) =>
148 d1.id() < d2.id() ? -1 : 1)
151 // De-flesh some values we don't want living directly on
152 // the copy. Cache the values so our comboboxes, etc.
153 // can use them without have to re-fetch them .
154 li.lineitem_details().forEach(copy => {
156 if ((val = copy.circ_modifier())) { // assignment
157 this.circModCache[val.code()] = copy.circ_modifier();
158 copy.circ_modifier(val.code());
160 if ((val = copy.fund())) {
161 this.fundCache[val.id()] = copy.fund();
164 if ((val = copy.location())) {
165 this.loc.locationCache[val.id()] = copy.location();
166 copy.location(val.id());
170 if (toCache) { this.liCache[li.id()] = liStruct; }
174 // Returns all matching attributes
175 // 'li' should be fleshed with attributes()
176 getAttributes(li: IdlObject, name: string, attrType?: string): IdlObject[] {
177 const values: IdlObject[] = [];
178 li.attributes().forEach(attr => {
179 if (attr.attr_name() === name) {
180 if (!attrType || attrType === attr.attr_type()) {
189 getAttributeValues(li: IdlObject, name: string, attrType?: string): string[] {
190 return this.getAttributes(li, name, attrType).map(attr => attr.attr_value());
193 // Returns the first matching attribute
194 // 'li' should be fleshed with attributes()
195 getFirstAttribute(li: IdlObject, name: string, attrType?: string): IdlObject {
196 return this.getAttributes(li, name, attrType)[0];
199 getFirstAttributeValue(li: IdlObject, name: string, attrType?: string): string {
200 const attr = this.getFirstAttribute(li, name, attrType);
201 return attr ? attr.attr_value() : '';
204 getOrderIdent(li: IdlObject): IdlObject {
205 for (let idx = 0; idx < li.attributes().length; idx++) {
206 const attr = li.attributes()[idx];
207 if (attr.order_ident() === 't' &&
208 attr.attr_type() === 'lineitem_local_attr_definition') {
215 // Returns an updated copy of the lineitem
216 changeOrderIdent(li: IdlObject,
217 id: number, attrType: string, attrValue: string): Observable<IdlObject> {
219 const args: any = {lineitem_id: li.id()};
222 // Order ident set to an existing attribute.
223 args.source_attr_id = id;
225 // Order ident set to a new free text value
226 args.attr_name = attrType;
227 args.attr_value = attrValue;
230 return this.net.request(
232 'open-ils.acq.lineitem.order_identifier.set',
233 this.auth.token(), args
234 ).pipe(switchMap(_ => this.getFleshedLineitems([li.id()]))
235 ).pipe(map(struct => struct.lineitem));
238 applyBatchNote(liIds: number[],
239 noteValue: string, vendorPublic: boolean): Promise<any> {
241 if (!noteValue || liIds.length === 0) { return Promise.resolve(); }
244 liIds.forEach(id => {
245 const note = this.idl.create('acqlin');
248 note.value(noteValue);
249 note.vendor_public(vendorPublic ? 't' : 'f');
253 return this.net.request('open-ils.acq',
254 'open-ils.acq.lineitem_note.cud.batch',
255 this.auth.token(), notes
257 if (resp && resp.note) {
258 const li = this.liCache[resp.note.lineitem()].lineitem;
259 li.lineitem_notes().unshift(resp.note);
264 getLiAttrDefs(): Promise<IdlObject[]> {
265 if (this.liAttrDefs) {
266 return Promise.resolve(this.liAttrDefs);
269 return this.pcrud.retrieveAll('acqliad', {}, {atomic: true})
270 .toPromise().then(defs => this.liAttrDefs = defs);
273 updateLiDetails(li: IdlObject): Observable<BatchLineitemUpdateStruct> {
274 const lids = li.lineitem_details().filter(copy =>
275 (copy.isnew() || copy.ischanged() || copy.isdeleted()));
277 return this.net.request(
279 'open-ils.acq.lineitem_detail.cud.batch', this.auth.token(), lids);
282 updateLineitems(lis: IdlObject[]): Observable<BatchLineitemUpdateStruct> {
284 // Fire updates one LI at a time. Note the API allows passing
285 // multiple LI's, but does not stream responses. This approach
286 // allows the caller to get a stream of responses instead of a
288 let obs: Observable<any> = empty();
290 obs = concat(obs, this.net.request(
292 'open-ils.acq.lineitem.update',
293 this.auth.token(), li