ACQ invoice inline lineitem search and add
authorBill Erickson <berick@esilibrary.com>
Tue, 10 Jul 2012 15:07:53 +0000 (11:07 -0400)
committerLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Wed, 1 Aug 2012 18:23:28 +0000 (14:23 -0400)
The Invoice UI is how composed of two tabs, the main invoice tab and a new
Search tab.  The search tab consists of a subset of the Acquisitions unified
search interface.  The goal is to allow users to search for lineitems to
invoice.  Search results may be added directly to the growing invoice.  A
number of small usability features are included.

Features
~~~~~~~~

    * Option (default) to limit searches to invoiceable items.
        ** These are lineitems that are not cancelled, have at least one
           invoiceable copy, linked to a PO whose provider matches that of the
           current invoice, and are not already linked to the current invoice.

    * Search defaults to last-run search (on workstation).
    * New Lineitem Detail filter options
    * Sort searches by lineitem number (default) and title.
    * There is a new Expected Cost field which includes both the total invoiced
      cost plus the anticipated cost of lineitems as they are added.
    * New Price per Copy field
    * Lineitem count field
    * Show / Hide Invoice details button.  Details are displayed by default, but
      hidden when the user enters the search tab.  From there it remains hidden
      until manually shown (or a new invoice is opened).
    * A new "Save & Clear" button which saves the current invoice then clears
      the invoice display to create a new invoice.
    * Provider, shipper, and receiver fields are auto-populated from the
      first-added invoice data (when not already set).
    * Totals are now read-only, since they are derived from existing data (and
      are informational only).

Signed-off-by: Bill Erickson <berick@esilibrary.com>
Signed-off-by: Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>

Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm
Open-ILS/src/templates/acq/invoice/view.tt2
Open-ILS/web/css/skin/default/acq.css
Open-ILS/web/js/ui/default/acq/invoice/common.js
Open-ILS/web/js/ui/default/acq/invoice/view.js
Open-ILS/web/js/ui/default/acq/search/unified.js
docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt [new file with mode: 0644]

index cbf4c73..da594a2 100644 (file)
@@ -229,7 +229,7 @@ sub prepare_terms {
     my $conj = $is_and ? "-and" : "-or";
     my $outer_clause = {};
 
-    foreach my $class (qw/acqpo acqpl acqinv jub/) {
+    foreach my $class (qw/acqpo acqpl acqinv jub acqlid acqlisum acqlisumi/) {
         next if not exists $terms->{$class};
 
         $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
@@ -243,7 +243,12 @@ sub prepare_terms {
             } elsif ($between and could_be_range($v)) {
                 $term_clause = {$k => {"between" => $v}};
             } elsif (check_1d_max($v)) {
-                $v = castdate($v, $gte, $lte) if $castdate;
+                if ($castdate) {
+                    $v = castdate($v, $gte, $lte) if $castdate;
+                } elsif ($gte or $lte) {
+                    my $op = $gte ? '>=' : '<=';
+                    $v = {$op => $v};
+                }
                 $term_clause = {$k => $v};
             } else {
                 next;
@@ -339,6 +344,11 @@ sub build_from_clause_and_joins {
         } else {
             $graft_map{$class} = $query->{from}{$core}{$class} ||= {};
             $graft_map{$class}{type} = $join_type;
+
+            # without this, the SQL attempts to join on 
+            # jub.order_summary, which is a virtual field.
+            $graft_map{$class}{field} = 'lineitem' 
+                if $class eq 'acqlisum' or $class eq 'acqlisumi';
         }
     }
 
@@ -415,6 +425,7 @@ sub unified_search {
         $hint => [{"column" => "id", "transform" => "distinct"}]
     };
 
+    my $attr_from_filter;
     if ($options->{"order_by"}) {
         # What's the point of this block?  When using ORDER BY in conjuction
         # with SELECT DISTINCT, the fields present in ORDER BY have to also
@@ -432,9 +443,30 @@ sub unified_search {
 q/order_by clause must be of the long form, like:
 "order_by": [{"class": "foo", "field": "bar", "direction": "asc"}]/
             );
+
         } else {
+
+            # we can't combine distinct(id) with another select column, 
+            # since the non-distinct column may arbitrarily (via hash keys)
+            # sort to the front of the final SQL, which PG will complain about.  
+            $select_clause = { $hint => ["id"] };
             $select_clause->{$class} ||= [];
-            push @{$select_clause->{$class}}, $field;
+            push @{$select_clause->{$class}}, 
+                {column => $field, transform => 'first', aggregate => 1};
+
+            # when sorting by LI attr values, we have to limit 
+            # to a specific type of attr value to sort on.
+            if ($class eq 'acqlia') {
+                $attr_from_filter = {
+                    "fkey" => "id",
+                    "filter" => {
+                        "attr_type" => "lineitem_marc_attr_definition",
+                        "attr_name" => $options->{"order_by_attr"} || "title"
+                    },
+                    "type" => "left",
+                    "field" =>"lineitem"
+                };
+            }
         }
     }
 
@@ -469,6 +501,14 @@ q/order_by clause must be of the long form, like:
         return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
     }
 
+
+    # if ordering by acqlia, insert the from clause 
+    # filter to limit to one type of attr.
+    if ($attr_from_filter) {
+        $query->{from}->{jub} = {} unless $query->{from}->{jub};
+        $query->{from}->{jub}->{acqlia} = $attr_from_filter;
+    }
+
     my $results = $e->json_query($query) or return $e->die_event;
     my @id_list = map { $_->{"id"} } (grep { $_->{"id"} } @$results);
 
index 7e4d489..6ed29f1 100644 (file)
 [% WRAPPER 'base.tt2' %]
 [% ctx.page_title = 'Invoicing' %]
-<script type="text/javascript">var invoiceId = '[% ctx.page_args.0 %]';</script>
 <div dojoType="dijit.layout.ContentPane" style="height:100%">
 
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
         <div> Invoice </div>
-        <div id="acq-view-invoice-receive" class="hidden"><button id="acq-view-invoice-receive-link">Receive Items</button></div>
+        <div id="acq-view-invoice-receive" class="hidden">
+            <button id="acq-view-invoice-receive-link">Receive Items</button>
+        </div>
     </div>
 
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
-        <div id='acq-view-invoice-div'></div>
-    </div>
+        <style>
+            #acq-invoice-num-summary-table td {
+                padding-right: 8px;
+                padding-top: 5px;
+                padding-bottom: 5px;
+                border-bottom: 2px solid #888;
+            }
+            #acq-invoice-num-summary-table td[id] {
+                font-weight:bold;
+            }
+        </style>
+        <table id='acq-invoice-num-summary-table'>
+            <tr><td>[% l("Lineitems: " ) %]</td><td id='acq-invoice-summary-count'>0</td></tr>
+            <tr>
+                <td>[% l("Expected Cost: " ) %]</td>
+                <td id='acq-invoice-summary-cost'>0.00</td>
+            </tr>
+        </table>
+        <br/>
+    <div>
 
     <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
-        <table class='oils-acq-invoice-table'>
-            <thead/>
-            <tbody id='acq-invoice-entry-header' class='hidden'>
-                <tr>
-                    <td colspan='0'>
-                        <h3>Bibliographic Items</h3>
-                    </td>
-                </tr>
-            </tbody>
-            <!-- acq.invoice_entry -->
-            <thead id='acq-invoice-entry-thead' class='hidden'>
-                <tr>
-                    <th colspan='2'>Title Details</th>
-                    <th class='acq-invoice-center-col'>#&nbsp;Invoiced&nbsp;/&nbsp;#&nbsp;Paid</th>
-                    <th class='acq-invoice-center-col'>Billed</th>
-                    <th class='acq-invoice-paid-col'>Paid</th>
-                    <th class='acq-invoice-center-col hide-complete'>Detach</th>
-                </tr>
-            </thead>
-            <tbody id='acq-invoice-entry-tbody' class='hidden'>
-                <tr id='acq-invoice-entry-template' class='acq-invoice-row'>
-                    <td colspan='2'>
-                        <div name='title_details'></div>
-                        <div name='note'></div>
-                    </td>
-                    <td class='acq-invoice-center-col'>
-                        <span name='inv_item_count'></span>&nbsp;/&nbsp;<span name='phys_item_count'></span>
-                    </td>
-                    <td class='acq-invoice-billed-col'><div name='cost_billed'></div></td>
-                    <td class='acq-invoice-paid-col'><div name='amount_paid'></div></td>
-                    <td class='acq-invoice-center-col hide-complete'><a href='javascript:void(0);' name='detach'>Detach</a></td>
-                </tr>
-            </tbody>
-            <tbody>
-                <tr>
-                    <td  style='margin-top:15px;' colspan='0'>
-                        <h3>Direct Charges, Taxes, Fees, etc.</h3>
-                    </td>
-                </tr>
-            </tbody>
-            <!-- acq.invoice_item -->
-            <thead>
-                <tr>
-                    <th>Charge Type</th>
-                    <th class='acq-invoice-center-col'>Fund</th>
-                    <th>Title/Description</th>
-                    <th class='acq-invoice-center-col'>Billed</th>
-                    <th class='acq-invoice-paid-col'>Paid</th>
-                    <th class='acq-invoice-center-col hide-complete'>Delete</th>
-                </tr>
-            </thead>
-            <tbody id='acq-invoice-item-tbody'>
-                <tr id='acq-invoice-item-template' class='acq-invoice-row acq-invoice-item-row'>
-                    <td><div name='inv_item_type'></div></td>
-                    <td class='acq-invoice-center-col'><div name='fund'></div></td>
-                    <td><div name='title'></div></td>
-                    <td class='acq-invoice-center-col acq-invoice-billed-col'><div name='cost_billed'></div></td>
-                    <td class='acq-invoice-paid-col'><div name='amount_paid'></div></td>
-                    <td class='acq-invoice-center-col hide-complete'><a href='javascript:void(0);' name='delete'>Delete</a></td>
-                </tr>
-            </tbody>
-            <tbody class='hide-complete'>
-                <tr>
-                    <td colspan='0'>
-                        <a href='javascript:void(0);' id='acq-invoice-new-item'>Add Charge...</a>
-                    </td>
-                </tr>
-            </tbody>
-            <tbody>
-                <tr>
-                    <td style='margin-top:15px;' colspan='0'> 
-                        <h3> </h3>
-                    </td>
-                </tr>
-            </tbody>
-            <thead>
-                <tr>
-                    <th colspan='3'/>
-                    <th class='acq-invoice-center-col acq-invoice-billed-col'>Total</th>
-                    <th class='acq-invoice-paid-col'>Total</th>
-                    <th class='acq-invoice-center-col acq-invoice-balance-col'>Balance</th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr>
-                    <td colspan='3' style='text-align:right;'>
-                        <button jsId='invoiceSaveButton' class='hide-complete'
-                            dojoType='dijit.form.Button' onclick='saveChanges();'>Save</button>
-                        <button jsId='invoiceProrateButton' class='hide-complete'
-                            dojoType='dijit.form.Button' onclick='saveChanges(true);'>Save &amp; Prorate</button>
-                        <button jsId='invoiceCloseButton' class='hide-complete'
-                            dojoType='dijit.form.Button' onclick='saveChanges(false, true);'>Save &amp; Close</button>
-                        <span class='hidden' id='acq-invoice-reopen-button-wrapper'>
-                            <button jsId='invoiceReopenButton' 
-                                dojoType='dijit.form.Button' onclick='saveChanges(false, false, true);'>Reopen Invoice</button>
-                        </span>
-                    </td>
-                    <td class='acq-invoice-center-col'><div jsId='totalInvoicedBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'></div></td>
-                    <td class='acq-invoice-paid-col'><div jsId='totalPaidBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'></div></td>
-                    <td class='acq-invoice-center-col'><div jsId='balanceOwedBox' dojoType='dijit.form.CurrencyTextBox' style='width:9em;'></div></td>
-                </tr>
-            </tbody>
-        </table>
+        <div id='acq-invoice-summary'>
+            <button id='acq-invoice-summary-toggle-off'>[% l('Hide Details') %]</button>
+            <div id='acq-view-invoice-div'></div>
+        </div>
+        <div id='acq-invoice-summary-small'>
+            <button id='acq-invoice-summary-toggle-on'>[% l('Show Details') %]</button>
+            <span style='font-weight:bold; font-size:120%' id='acq-invoice-summary-name'></span>
+            <br/>
+            <br/>
+        </div>
     </div>
+
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+
+
+        <div dojoType="dijit.layout.TabContainer" style="width: 96%; height: 100%;">
+            <div dojoType="dijit.layout.ContentPane" 
+                class='oils-acq-detail-content-pane' title="Invoice" selected='true' style='height:600px'>
+
+                <script type='dojo/connect' event='onShow'>
+                    // the table is left at display=none on subsequent tab views
+                    dojo.byId('oils-acq-invoice-table').style.display = 'table'
+                </script>
+
+                <table id='oils-acq-invoice-table' class='oils-acq-invoice-table'>
+                    <thead/>
+                    <tbody id='acq-invoice-entry-header' class='hidden'>
+                        <tr>
+                            <td colspan='0'>
+                                <h3>
+                                    [% l('Bibliographic Items') %]
+                                </h3>
+                            </td>
+                        </tr>
+                    </tbody>
+                    <!-- acq.invoice_entry -->
+                    <thead id='acq-invoice-entry-thead' class='hidden'>
+                        <tr>
+                            <th colspan='2'>Title Details</th>
+                            <th class='acq-invoice-center-col'>#&nbsp;Invoiced&nbsp;/&nbsp;#&nbsp;Paid</th>
+                            <th class='acq-invoice-center-col'>Billed</th>
+                            <th class='acq-invoice-paid-per-copy-col'>Per Copy</th>
+                            <th class='acq-invoice-paid-col'>Paid</th>
+                            <th class='acq-invoice-center-col hide-complete'>Detach</th>
+                        </tr>
+                    </thead>
+                    <tbody id='acq-invoice-entry-tbody' class='hidden'>
+                        <tr id='acq-invoice-entry-template' class='acq-invoice-row'>
+                            <td colspan='2'>
+                                <div name='title_details'></div>
+                                <div name='note'></div>
+                            </td>
+                            <td class='acq-invoice-center-col'>
+                                <span name='inv_item_count'></span>&nbsp;/&nbsp;<span name='phys_item_count'></span>
+                            </td>
+                            <td class='acq-invoice-billed-col'><div name='cost_billed'></div></td>
+                            <td><div name='amount_paid_per_copy'>0.00</div></td>
+                            <td class='acq-invoice-paid-col'><div name='amount_paid'></div></td>
+                            <td class='acq-invoice-center-col hide-complete'><a href='javascript:void(0);' name='detach'>Detach</a></td>
+                        </tr>
+                    </tbody>
+                    <tbody>
+                        <tr>
+                            <td  style='margin-top:15px;' colspan='0'>
+                                <h3>Direct Charges, Taxes, Fees, etc.</h3>
+                            </td>
+                        </tr>
+                    </tbody>
+                    <!-- acq.invoice_item -->
+                    <thead>
+                        <tr>
+                            <th>Charge Type</th>
+                            <th class='acq-invoice-center-col'>Fund</th>
+                            <th>Title/Description</th>
+                            <th class='acq-invoice-center-col'>Billed</th>
+                            <th/>
+                            <th class='acq-invoice-paid-col'>Paid</th>
+                            <th class='acq-invoice-center-col hide-complete'>Delete</th>
+                        </tr>
+                    </thead>
+                    <tbody id='acq-invoice-item-tbody'>
+                        <tr id='acq-invoice-item-template' class='acq-invoice-row acq-invoice-item-row'>
+                            <td><div name='inv_item_type'></div></td>
+                            <td class='acq-invoice-center-col'><div name='fund'></div></td>
+                            <td><div name='title'></div></td>
+                            <td class='acq-invoice-center-col acq-invoice-billed-col'><div name='cost_billed'></div></td>
+                            <td/>
+                            <td class='acq-invoice-paid-col'><div name='amount_paid'></div></td>
+                            <td class='acq-invoice-center-col hide-complete'><a href='javascript:void(0);' name='delete'>Delete</a></td>
+                        </tr>
+                    </tbody>
+                    <tbody class='hide-complete'>
+                        <tr>
+                            <td colspan='0'>
+                                <a href='javascript:void(0);' id='acq-invoice-new-item'>Add Charge...</a>
+                            </td>
+                        </tr>
+                    </tbody>
+                    <tbody>
+                        <tr>
+                            <td style='margin-top:15px;' colspan='0'> 
+                                <h3> </h3>
+                            </td>
+                        </tr>
+                    </tbody>
+                    <thead>
+                        <tr>
+                            <th colspan='3'/>
+                            <th class='acq-invoice-center-col acq-invoice-billed-col'>Total</th>
+                            <th/>
+                            <th class='acq-invoice-paid-col'>Total</th>
+                            <th class='acq-invoice-center-col acq-invoice-balance-col'>Balance</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td colspan='3' style='text-align:right;'>
+                                <button jsId='invoiceSaveButton' class='hide-complete'
+                                    dojoType='dijit.form.Button' onclick='saveChanges();'>[% l('Save') %]</button>
+                                <button jsId='invoiceSaveButton' class='hide-complete'
+                                    dojoType='dijit.form.Button' onclick='saveChanges({clear:true});'>[% l('Save &amp; Clear') %]</button>
+                                <button jsId='invoiceProrateButton' class='hide-complete'
+                                    dojoType='dijit.form.Button' onclick='saveChanges({prorate:true});'>[% l('Prorate') %]</button>
+                                <button jsId='invoiceCloseButton' class='hide-complete'
+                                    dojoType='dijit.form.Button' onclick='saveChanges({close:true});'>[% l('Close') %]</button>
+                                <span class='hidden' id='acq-invoice-reopen-button-wrapper'>
+                                    <button jsId='invoiceReopenButton' 
+                                        dojoType='dijit.form.Button' onclick='saveChanges({reopen:true});'>[% l('Reopen') %]</button>
+                                </span>
+                            </td>
+                            <td class='acq-invoice-center-col'><div id='acq-total-invoiced-box'></div></td>
+                            <td/>
+                            <td class='acq-invoice-paid-col'><div id='acq-total-paid-box'></div></td>
+                            <td class='acq-invoice-center-col'><div id='acq-total-balance-box'></div></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div> <!-- tab 1 -->
+
+            <div dojoType="dijit.layout.ContentPane" 
+                class='oils-acq-detail-content-pane' title="Search">
+    
+                <script type='dojo/connect' event='onShow'>
+                    // hide summary info when opening the search tab
+                    dojo.byId('acq-invoice-summary-toggle-off').onclick();
+                    renderUnifiedSearch();
+                </script>
+
+
+                <!-- slim, inline unified search UI -->
+                <div id='oils-acq-invoice-search' _class='hidden'>
+
+                    <div id="acq-unified-form">
+                        <div>
+                            <label for="acq-unified-result-type">[% l('Search for') %]</label>
+                            <select id="acq-unified-result-type" disabled='disabled'>
+                                <option value="lineitem">line items</option>
+                            </select>
+                            <label for="acq-unified-conjunction">matching</label>
+                            <select id="acq-unified-conjunction">
+                                <option value="and">all</option>
+                                <option value="or">any</option>
+                            </select>
+                            <label for="acq-unified-conjunction">
+                                of the following terms:
+                            </label>
+                        </div>
+                        <div id="acq-unified-terms">
+                            <table id="acq-unified-terms-table">
+                                <tbody id="acq-unified-terms-tbody">
+                                    <tr id="acq-unified-terms-row-tmpl"
+                                        class="acq-unified-terms-row">
+                                        <td name="selector"
+                                            class="acq-unified-terms-selector"></td>
+                                        <td name="match"
+                                            class="acq-unified-terms-match">
+                                            <select>
+                                                <option value="">is</option>
+                                                <option value="__not">is NOT</option>
+                                                <option value="__fuzzy" disabled="disabled">
+                                                    contains
+                                                </option>
+                                                <option value="__not,__fuzzy"
+                                                    disabled="disabled">
+                                                    does NOT contain
+                                                </option>
+                                                <option value="__lte" disabled="disabled">
+                                                    is on or BEFORE
+                                                </option>
+                                                <option value="__gte" disabled="disabled">
+                                                    is on or AFTER
+                                                </option>
+                                                <option value="__in" disabled="disabled">
+                                                    matches a term from a file
+                                                </option>
+                                            </select>
+                                        </td>
+                                        <td name="widget"
+                                            class="acq-unified-terms-widget"></td>
+                                        <td name="remove"
+                                            class="acq-unified-terms-remove"></td>
+                                    </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                        <div id="acq-unified-add-term">
+                            <button onclick="termManager.addRow()">Add Search Term</button>
+                        </div>
+                        <table width='100%'><tr>
+                            <td align='left'>
+                                <button onclick="performSearch(0)">[% l('Search') %]</button>
+                                <button onclick='addSelectedToInvoice()'>
+                                    [% l('Add Selected Items to Invoice') %]
+                                </button>
+                                <span id='acq-inv-search-prev'>
+                                    <a href='javascript:performSearch(-1)'>[% l('Previous') %]</a>
+                                </span>
+                                <span>
+                                    <a href='javascript:performSearch(1)'>[% l('Next') %]</a>
+                                </span>
+                            </td>
+                            <td align='right'>
+                                <input type='checkbox' id='acq-invoice-search-sort-title'/>
+                                [% l('Sort by title') %]
+                                <span style='padding-left:8px;'>
+                                <input type='checkbox' id='acq-invoice-search-limit-invoiceable' checked='checked'/>
+                                [% l('Limit to Invoiceable Items') %]
+                            </td>
+                        </tr></table>
+                    </div> <!-- end search form -->
+                    <div id='acq-unified-results-no_results'>
+                        <b>[% l('No Results') %]</b>
+                    </div>
+                    <div id='acq-unified-results-lineitem'>
+                        <style>
+                            #acq-invoice-search-results-tbody  { width: 100%; }
+                            #acq-invoice-search-results-tbody td { 
+                                padding: 5px; 
+                                border-bottom: 1px solid #888;
+                            }
+                            .search-resutls-select-td {
+                                padding-right: 8px; border-right: 2px solid #888;
+                            }
+                            .search-results-content-td {
+                                padding-left: 8px; border-left: 2px solid #888; 
+                            }
+                            .search-results-already-invoiced {
+                                background-color: #E99;
+                            }
+                        </style>
+                        <table>
+                            <tbody id='acq-invoice-search-results-tbody'>
+                                <tr id='acq-invoice-search-results-tr'>
+                                    <td class='search-resutls-select-td'>
+                                        <input type='checkbox' name='search-results-checkbox'/>
+                                    </td>
+                                    <td class='search-results-content-td'>
+                                        <div name='search-results-content-div'>
+                                            <img src='[% ctx.media_prefix %]/opac/images/progressbar_green.gif'/>
+                                        </div>
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div> <!-- tab 2 -->
+        </div> <!-- end tabcontainer -->
+    </div> <!-- end contentpane -->
+
 </div>
 <div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div>
 <div jsId='extraItemsDialog' dojoType="dijit.Dialog" title="Extra Items">
         <button dojoType='dijit.form.Button' jsId='extraCopiesGo'>Add New Items</button>
     </div>
 </div>
+<script type="text/javascript">
+    var invoiceId = '[% ctx.page_args.0 %]';
+    window.unifiedSearchExternalMode = true;
+</script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/acq/common/base64.js"></script>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/acq/search/unified.js"></script>
 <script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/acq/invoice/common.js'> </script>
 <script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/acq/invoice/view.js'> </script>
 [% END %]
index 6d03933..07fccb8 100644 (file)
@@ -211,7 +211,7 @@ span[name="notes_alert_flag"] {color: #c00;font-weight: bold;font-size: 110%;mar
 .acq-inoice-item-extra-info { padding-left: 10px; }
 .acq-inoice-item-info { font-weight: bold; }
 .acq-invoice-row td { border-bottom: 1px solid #e0e0e0; }
-.acq-invoice-invalid-amount input { color: red; font-weight: bold; }
+.acq-invoice-invalid-amount  { color: red; font-weight: bold; }
 .acq-link-invoice-dialog td,.acq-link-invoice-dialog th {padding-top: 10px;}
 .acq-invoice-paid-col {background : #E0E0E0; text-align: center;}
 .acq-invoice-center-col { text-align: center; }
index 03fade4..0980fca 100644 (file)
@@ -4,18 +4,26 @@ dojo.require('openils.widget.EditPane');
 
 function drawInvoicePane(parentNode, inv, args) {
     args = args || {};
+    var pane;
 
     var override = {};
     if(!inv) {
         override = {
             recv_date : {widgetValue : dojo.date.stamp.toISOString(new Date())},
-            receiver : {widgetValue : openils.User.user.ws_ou()},
+            //receiver : {widgetValue : openils.User.user.ws_ou()},
             recv_method : {widgetValue : 'PPR'}
         };
     }
 
     dojo.mixin(override, {
-        provider : { dijitArgs : { store_options : { base_filter : { active :"t" } } } },
+        provider : { 
+            dijitArgs : { 
+                store_options : { base_filter : { active :"t" } },
+                onChange : function(val) {
+                    pane.setFieldValue('shipper', val);
+                }
+            } 
+        },
         shipper  : { dijitArgs : { store_options : { base_filter : { active :"t" } } } }
     });
 
@@ -23,7 +31,19 @@ function drawInvoicePane(parentNode, inv, args) {
         override[field] = {widgetValue : args[field]};
     }
 
-    var pane = new openils.widget.EditPane({
+    // push the name of the invoice into the name display field after update
+    override.inv_ident = dojo.mixin(
+        override.inv_ident,
+        {dijitArgs : {onChange :
+            function(newVal) {
+                if (dojo.byId('acq-invoice-summary-name'))
+                    dojo.byId('acq-invoice-summary-name').innerHTML = newVal;
+            }
+        }}
+    );
+
+
+    pane = new openils.widget.EditPane({
         fmObject : inv,
         paneStackCount : 2,
         fmClass : 'acqinv',
index 2e8f499..4f1fa60 100644 (file)
@@ -1,6 +1,8 @@
 dojo.require('dojo.date.locale');
 dojo.require('dojo.date.stamp');
+dojo.require('dojo.cookie');
 dojo.require('dijit.form.CheckBox');
+dojo.require('dijit.form.Button');
 dojo.require('dijit.form.CurrencyTextBox');
 dojo.require('dijit.form.NumberTextBox');
 dojo.require('openils.User');
@@ -36,6 +38,9 @@ var extraCopies = {};
 var extraCopiesFund;
 var widgetRegistry = {acqie : {}, acqii : {}};
 var focusLineitem;
+var searchInitDone = false;
+var termManager;
+var resultManager;
 
 function nodeByName(name, context) {
     return dojo.query('[name='+name+']', context)[0];
@@ -53,12 +58,30 @@ function init() {
 
     focusLineitem = new openils.CGI().param('focus_li');
 
+    totalInvoicedBox = dojo.byId('acq-total-invoiced-box');
+    totalPaidBox = dojo.byId('acq-total-paid-box');
+    balanceOwedBox = dojo.byId('acq-total-balance-box');
+
     itemTypes = pcrud.retrieveAll('aiit');
 
+    dojo.byId('acq-invoice-summary-toggle-off').onclick = function() {
+        openils.Util.hide(dojo.byId('acq-invoice-summary'));
+        openils.Util.show(dojo.byId('acq-invoice-summary-small'));
+    };
+
+    dojo.byId('acq-invoice-summary-toggle-on').onclick = function() {
+        openils.Util.show(dojo.byId('acq-invoice-summary'));
+        openils.Util.hide(dojo.byId('acq-invoice-summary-small'));
+    }
+
     if(cgi.param('create')) {
         renderInvoice();
 
+        // show summary info by default for new invoices
+        dojo.byId('acq-invoice-summary-toggle-on').onclick();
+
     } else {
+        dojo.byId('acq-invoice-summary-toggle-off').onclick();
         fieldmapper.standardRequest(
             ['open-ils.acq', 'open-ils.acq.invoice.retrieve.authoritative'],
             {
@@ -117,16 +140,20 @@ function renderInvoice() {
         );
     }
 
+    // display items and entries in ID order 
+    // which effectively equates to add order.
+    function idsort(a, b) { return a.id() < b.id() ? -1 : 1 }
+
     if(invoice) {
         dojo.forEach(
-            invoice.items(),
+            invoice.items().sort(idsort),
             function(item) {
                 addInvoiceItem(item);
             }
         );
 
         dojo.forEach(
-            invoice.entries(),
+            invoice.entries().sort(idsort),
             function(entry) {
                 addInvoiceEntry(entry);
             }
@@ -137,10 +164,10 @@ function renderInvoice() {
     if(attachPo.length) doAttachPo(0);
 }
 
-function doAttachLi() {
+function doAttachLi(skipInit) {
 
     //var invoiceArgs = {provider : lineitem.provider(), shipper : lineitem.provider()}; 
-    if(cgi.param('create')) {
+    if(cgi.param('create') && !skipInit) {
 
         // use the first LI in the list to determine the default provider
         fieldmapper.standardRequest(
@@ -223,6 +250,172 @@ function doAttachPo(idx) {
     );
 }
 
+function performSearch(pageDir) {
+    clearSearchResTable(); 
+    var searchObject = termManager.buildSearchObject();
+    dojo.cookie('invs', base64Encode(searchObject));
+    dojo.cookie('invc', dojo.byId("acq-unified-conjunction").getValue());
+
+    if (pageDir == 0) { // new search
+        resultsLoader.displayOffset = 0;
+    } else {
+        resultsLoader.displayOffset += pageDir * resultsLoader.displayLimit;
+    }
+
+    if (resultsLoader.displayOffset == 0) {
+        openils.Util.hide('acq-inv-search-prev');
+    } else {
+        openils.Util.show('acq-inv-search-prev', 'inline');
+    }
+
+    if (dojo.byId('acq-invoice-search-limit-invoiceable').checked) {
+        if (!searchObject.jub) 
+            searchObject.jub = [];
+
+        // exclude lineitems that are "cancelled" (sidebar: 'Mericans spell it 'canceled')
+        searchObject.jub.push({state : 'cancelled', '__not' : true});
+
+        // exclude lineitems already linked to this invoice
+        if (invoice && invoice.id() > 0) { 
+            if (!searchObject.acqinv)
+                searchObject.acqinv = [];
+            searchObject.acqinv.push({id : invoice.id(), '__not' : true});
+        }
+
+        // limit to lineitems that have invoiceable copies
+        searchObject.acqlisumi = [{item_count : 1, '_gte' : true}];
+
+        // limit to provider if a provider is selected
+        var provider = invoicePane.getFieldValue('provider');
+        if (provider) {
+            if (!searchObject.jub.filter(function(i) { return i.provider != null }).length)
+                searchObject.jub.push({provider : provider});
+        }
+    }
+
+    if (dojo.byId('acq-invoice-search-sort-title').checked) {
+        uriManager.order_by = 
+            [ {"class": "acqlia", "field":"attr_value", "transform":"first"} ];
+    }
+
+    resultsLoader.lastSearch = searchObject;
+    resultManager.go(searchObject)
+    console.log('Lineitem Search: ' + js2JSON(searchObject));
+    focusLastSearchInput();
+}
+
+
+function renderUnifiedSearch() {
+
+    if (!searchInitDone) {
+
+        searchInitDone = true;
+        termManager = new TermManager();
+        resultManager = new ResultManager();
+        resultsLoader = new searchResultsLoader();
+        uriManager = new URIManager();
+
+        // define custom lineitem result handler
+        resultManager.result_types = {
+            "lineitem": {
+                "search_options": { "id_list": true },
+                "revealer": function() { },
+                "finisher": function() {
+                    resultsLoader.batch_length = resultManager.count_results;
+                },
+                "adder": function(li) {
+                    resultsLoader.addLineitem(li);
+                },
+                "interface": resultsLoader
+            },
+            "no_results": {
+                "revealer": function() { }
+            }
+        };
+
+        var searchObject = dojo.cookie('invs');
+        console.log('loaded ' + searchObject);
+        if (searchObject) {
+            // if there is a search object cookie, populate the search form
+            termManager.reflect(base64Decode(searchObject));
+            dojo.byId("acq-unified-conjunction").setValue(dojo.cookie('invc'));
+        } else {
+            console.log('adding row');
+            termManager.addRow();
+        }
+    }
+
+    dojo.addClass(dojo.byId('oils-acq-invoice-table'), 'hidden');
+    dojo.removeClass(dojo.byId('oils-acq-invoice-search'), 'hidden');
+    focusLastSearchInput();
+}
+
+function focusLastSearchInput() {
+    // TODO: see about making this better and moving it into search/unified.js
+    var wnodes = dojo.query('[name=widget]');
+    var inputNode = wnodes.item(wnodes.length - 1).firstChild;
+    if (inputNode) {
+        try {
+            inputNode.select();
+        } catch(E) {
+            inputNode.focus();
+        }
+    }
+}
+
+var resultsTbody, resultsRow;
+function searchResultsLoader() {
+    this.displayOffset = 0;
+    this.displayLimit = 10;
+
+    if (!resultsTbody) {
+        resultsTbody = dojo.byId('acq-invoice-search-results-tbody');
+        resultsRow = resultsTbody.removeChild(dojo.byId('acq-invoice-search-results-tr'));
+    }
+
+    this.addLineitem = function(li_id) {
+        console.log('Adding search result lineitem ' + li_id);
+        var row = resultsRow.cloneNode(true);
+        resultsTbody.appendChild(row);
+        var checkbox = dojo.query('[name=search-results-checkbox]', row)[0];
+        checkbox.setAttribute('lineitem', li_id);
+
+        // this lineitem is already part of the invoice
+        if (dojo.query('[entry_lineitem_row=' + li_id + ']')[0]) {
+            checkbox.disabled = true;
+            dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
+        }
+
+        openils.acq.Lineitem.fetchAndRender(
+            li_id, {}, 
+            function(li, html) { 
+                dojo.query('[name=search-results-content-div]', row)[0].innerHTML = html;
+            }
+        );
+    }
+}
+
+function addSelectedToInvoice() {
+    var inputs = dojo.query('[name=search-results-checkbox]');
+    attachLi = [];
+    dojo.forEach(inputs,
+        function(checkbox) {
+            if (checkbox.checked) {
+                attachLi.push(checkbox.getAttribute('lineitem'));
+                checkbox.disabled = true;
+                checkbox.checked = false;
+                dojo.addClass(checkbox.parentNode, 'search-results-already-invoiced');
+            }
+        }
+    );
+    doAttachLi(true);
+}
+
+function clearSearchResTable() {
+    while (resultsTbody.childNodes[0])
+        resultsTbody.removeChild(resultsTbody.childNodes[0]);
+}
+
 function updateTotalCost() {
 
     var totalCost = 0;    
@@ -232,7 +425,7 @@ function updateTotalCost() {
     for(var id in widgetRegistry.acqie) 
         if(!widgetRegistry.acqie[id]._object.isdeleted())
             totalCost += Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue());
-    totalInvoicedBox.attr('value', totalCost);
+    totalInvoicedBox.innerHTML = totalCost.toFixed(2);
 
     totalPaid = 0;    
     for(var id in widgetRegistry.acqii) 
@@ -241,27 +434,27 @@ function updateTotalCost() {
     for(var id in widgetRegistry.acqie) 
         if(!widgetRegistry.acqie[id]._object.isdeleted())
             totalPaid += Number(widgetRegistry.acqie[id].amount_paid.getFormattedValue());
-    totalPaidBox.attr('value', totalPaid);
+    totalPaidBox.innerHTML = totalPaid.toFixed(2);
 
     var buttonsDisabled = false;
 
     if(totalPaid > totalCost || totalPaid < 0) {
-        openils.Util.addCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
+        openils.Util.addCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
         invoiceSaveButton.attr('disabled', true);
         invoiceProrateButton.attr('disabled', true);
         buttonsDisabled = true;
     } else {
-        openils.Util.removeCSSClass(totalPaidBox.domNode, 'acq-invoice-invalid-amount');
+        openils.Util.removeCSSClass(totalPaidBox, 'acq-invoice-invalid-amount');
         invoiceSaveButton.attr('disabled', false);
         invoiceProrateButton.attr('disabled', false);
     }
 
     if(totalCost < 0) {
-        openils.Util.addCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
+        openils.Util.addCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
         invoiceSaveButton.attr('disabled', true);
         invoiceProrateButton.attr('disabled', true);
     } else {
-        openils.Util.removeCSSClass(totalInvoicedBox.domNode, 'acq-invoice-invalid-amount');
+        openils.Util.removeCSSClass(totalInvoicedBox, 'acq-invoice-invalid-amount');
         if(!buttonsDisabled) {
             invoiceSaveButton.attr('disabled', false);
             invoiceProrateButton.attr('disabled', false);
@@ -274,7 +467,9 @@ function updateTotalCost() {
         invoiceCloseButton.attr('disabled', true);
     }
 
-    balanceOwedBox.attr('value', (totalCost - totalPaid));
+    balanceOwedBox.innerHTML = (totalCost - totalPaid).toFixed(2);
+
+    updateExpectedCost();
 }
 
 
@@ -316,6 +511,7 @@ function addInvoiceItem(item) {
             } else if(field == 'cost_billed' || field == 'amount_paid') {
                 args = {required : true, style : 'width: 8em'};
             }
+
             registerWidget(
                 item,
                 field,
@@ -481,12 +677,39 @@ function focusLi() {
 }
 
 
+// expected cost is totalCostInvoiced + totalCostNotYetInvoiced
+function updateExpectedCost() {
+
+    var cost = Number(totalInvoicedBox.innerHTML || 0);
+
+    // for any LI's that are not yet billed (i.e. filled in)
+    // use the total expected cost for that lineitem.
+    for(var id in widgetRegistry.acqie) {
+        var entry = widgetRegistry.acqie[id]._object;
+        if(!entry.isdeleted()) {
+            if (Number(widgetRegistry.acqie[id].cost_billed.getFormattedValue()) == 0) {
+                var li = entry.lineitem();
+                cost += 
+                    Number(li.order_summary().estimated_amount()) - 
+                    Number(li.order_summary().paid_amount());
+            }
+        }
+    }
+
+    dojo.byId('acq-invoice-summary-cost').innerHTML = cost.toFixed(2);
+}
+
+var invoicEntryWidgets = {};
 function addInvoiceEntry(entry) {
+    console.log('Adding new entry for lineitem ' + entry.lineitem());
 
     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-header'), 'hidden');
     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-thead'), 'hidden');
     openils.Util.removeCSSClass(dojo.byId('acq-invoice-entry-tbody'), 'hidden');
 
+    dojo.byId('acq-invoice-summary-count').innerHTML = 
+        Number(dojo.byId('acq-invoice-summary-count').innerHTML) + 1;
+
     entryTbody = dojo.byId('acq-invoice-entry-tbody');
     if(entryTemplate == null) {
         entryTemplate = entryTbody.removeChild(dojo.byId('acq-invoice-entry-template'));
@@ -498,6 +721,7 @@ function addInvoiceEntry(entry) {
 
     var row = entryTemplate.cloneNode(true);
     row.setAttribute('lineitem', entry.lineitem());
+    row.setAttribute('entry_lineitem_row', entry.lineitem());
 
     openils.acq.Lineitem.fetchAndRender(
         entry.lineitem(), {}, 
@@ -511,6 +735,14 @@ function addInvoiceEntry(entry) {
 
             updateReceiveLink(li);
 
+            // set some default values if otherwise unset
+            if (!invoicePane.getFieldValue('receiver')) {
+                invoicePane.setFieldValue('receiver', li.purchase_order().ordering_agency());
+            }
+            if (!invoicePane.getFieldValue('provider')) {
+                invoicePane.setFieldValue('provider', li.purchase_order().provider());
+            }
+
             dojo.forEach(
                 ['inv_item_count', 'phys_item_count', 'cost_billed', 'amount_paid'],
                 function(field) {
@@ -536,6 +768,7 @@ function addInvoiceEntry(entry) {
                             parentNode : nodeByName(field, row)
                         }),
                         function(w) {    
+
                             if(field == 'phys_item_count') {
                                 dojo.connect(w, 'onChange', 
                                     function() {
@@ -548,12 +781,20 @@ function addInvoiceEntry(entry) {
                                         }
                                     }
                                 )
-                            }
-                        }
+                            } // if
+
+                            if(field == 'inv_item_count' || field == 'cost_billed') {
+                                setPerCopyPrice(row, entry);
+                                // update the per-copy count as invoice count and cost billed change 
+                                dojo.connect(w, 'onChange', function() { setPerCopyPrice(row, entry) } );
+                            } 
+
+                        } // func
                     );
                 }
             );
 
+            updateTotalCost();
             if (focusLineitem == li.id())
                 focusLi();
         }
@@ -586,7 +827,22 @@ function addInvoiceEntry(entry) {
     }
 
     entryTbody.appendChild(row);
-    updateTotalCost();
+}
+
+function setPerCopyPrice(row, entry) {
+    var inv_w = widgetRegistry.acqie[entry.id()].inv_item_count;
+    var bill_w = widgetRegistry.acqie[entry.id()].cost_billed;
+
+    if (inv_w && bill_w) {
+        var invoiced = Number(inv_w.getFormattedValue());
+        var billed = Number(bill_w.getFormattedValue());
+        console.log(invoiced + ' : ' + billed);
+        if (invoiced > 0) {
+            nodeByName('amount_paid_per_copy', row).innerHTML = (billed / invoiced).toFixed(2);
+        } else {
+            nodeByName('amount_paid_per_copy', row).innerHTML = '0.00';
+        }
+    }
 }
 
 function liMarcAttr(lineitem, name) {
@@ -601,12 +857,9 @@ function liMarcAttr(lineitem, name) {
     return (attr) ? attr.attr_value() : '';
 }
 
-function saveChanges(doProrate, doClose, doReopen) {
-    createExtraCopies(
-        function() {
-            saveChangesPartTwo(doProrate, doClose, doReopen);
-        }
-    );
+function saveChanges(args) {
+    args = args || {};
+    createExtraCopies(function() { saveChangesPartTwo(args); });
 }
 
 // Define a helper function to 'unflesh' sub-objects from an fmclass object.
@@ -621,10 +874,10 @@ function unflesh() {
     });
 }
 
-function saveChangesPartTwo(doProrate, doClose, doReopen) {
-    
+function saveChangesPartTwo(args) {
+    args = args || {};
 
-    if(doReopen) {
+    if(args.reopen) {
         invoice.complete('f');
 
     } else {
@@ -643,7 +896,7 @@ function saveChangesPartTwo(doProrate, doClose, doReopen) {
             return;
         }
 
-        if(doClose)
+        if(args.close)
             invoice.complete('t');
 
 
@@ -693,9 +946,13 @@ function saveChangesPartTwo(doProrate, doClose, doReopen) {
                 progressDialog.hide();
                 var invoice = openils.Util.readResponse(r);
                 if(invoice) {
-                    if(doProrate)
+                    if(args.prorate)
                         return prorateInvoice(invoice);
-                    location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
+                    if (args.clear) {
+                        location.href = oilsBasePath + '/acq/invoice/view?create=1';
+                    } else {
+                        location.href = oilsBasePath + '/acq/invoice/view/' + invoice.id();
+                    }
                 }
             }
         }
index b7f0137..2c1eff0 100644 (file)
@@ -4,6 +4,7 @@ dojo.require("openils.widget.AutoGrid");
 dojo.require("openils.widget.AutoWidget");
 dojo.require("openils.widget.XULTermLoader");
 dojo.require("openils.PermaCrud");
+dojo.require('dijit.layout.TabContainer');
 
 if (!localeStrings) {   /* we can do this because javascript doesn't have block 
                            scope */
@@ -251,7 +252,7 @@ function TermManager() {
     };
 
     this.terms = {};
-    ["jub", "acqpl", "acqpo", "acqinv"].forEach(
+    ["jub", "acqpl", "acqpo", "acqinv", "acqlid"].forEach(
         function(hint) {
             var o = {};
             o.__label = fieldmapper.IDL.fmclasses[hint].label;
@@ -621,84 +622,91 @@ function ResultManager(liPager, poGrid, plGrid, invGrid) {
     this.plCache = {};
     this.invCache = {};
 
-    this.result_types = {
-        "lineitem": {
-            "search_options": {
-                "flesh_attrs": true,
-                "flesh_cancel_reason": true,
-                "flesh_notes": true
-            },
-            "revealer": function() {
-                self.liPager.show();
-                progressDialog.show(true);
-            },
-            "finisher": function() {
-                self.liPager.batch_length = self.count_results;
-                self.liPager.relabelControls();
-                self.liPager.enableControls(true);
-                progressDialog.hide();
-            },
-            "adder": function(li) {
-                self.liPager.liTable.addLineitem(li);
-            },
-            "interface": self.liPager
-        },
-        "purchase_order": {
-            "search_options": {
-                "no_flesh_cancel_reason": true
-            },
-            "revealer": function() {
-                self.poGrid.resetStore();
-                self.poGrid.showLoadProgressIndicator();
-                self.poCache = {};
-            },
-            "finisher": function() {
-                self.poGrid.hideLoadProgressIndicator();
-            },
-            "adder": function(po) {
-                self.poCache[po.id()] = po;
-                self.poGrid.store.newItem(acqpo.toStoreItem(po));
-            },
-            "interface": self.poGrid
-        },
-        "picklist": {
-            "search_options": {
-                "flesh_lineitem_count": true,
-                "flesh_owner": true
-            },
-            "revealer": function() {
-                self.plGrid.resetStore();
-                self.plGrid.showLoadProgressIndicator();
-                self.plCache = {};
-            },
-            "finisher": function() {
-                self.plGrid.hideLoadProgressIndicator();
-            },
-            "adder": function(pl) {
-                self.plCache[pl.id()] = pl;
-                self.plGrid.store.newItem(acqpl.toStoreItem(pl));
-            },
-            "interface": self.plGrid
-        },
-        "invoice": {
-            "search_options": {
-                "no_flesh_misc": true
+    if (window.unifiedSearchExternalMode) {
+
+        // external user will define result types and handlers
+
+    } else {
+
+        this.result_types = {
+            "lineitem": {
+                "search_options": {
+                    "flesh_attrs": true,
+                    "flesh_cancel_reason": true,
+                    "flesh_notes": true
+                },
+                "revealer": function() {
+                    self.liPager.show();
+                    progressDialog.show(true);
+                },
+                "finisher": function() {
+                    self.liPager.batch_length = self.count_results;
+                    self.liPager.relabelControls();
+                    self.liPager.enableControls(true);
+                    progressDialog.hide();
+                },
+                "adder": function(li) {
+                    self.liPager.liTable.addLineitem(li);
+                },
+                "interface": self.liPager
             },
-            "finisher": function() {
-                self.invGrid.hideLoadProgressIndicator();
+            "purchase_order": {
+                "search_options": {
+                    "no_flesh_cancel_reason": true
+                },
+                "revealer": function() {
+                    self.poGrid.resetStore();
+                    self.poGrid.showLoadProgressIndicator();
+                    self.poCache = {};
+                },
+                "finisher": function() {
+                    self.poGrid.hideLoadProgressIndicator();
+                },
+                "adder": function(po) {
+                    self.poCache[po.id()] = po;
+                    self.poGrid.store.newItem(acqpo.toStoreItem(po));
+                },
+                "interface": self.poGrid
             },
-            "revealer": function() {
-                self.invGrid.resetStore();
-                self.invCache = {};
+            "picklist": {
+                "search_options": {
+                    "flesh_lineitem_count": true,
+                    "flesh_owner": true
+                },
+                "revealer": function() {
+                    self.plGrid.resetStore();
+                    self.plGrid.showLoadProgressIndicator();
+                    self.plCache = {};
+                },
+                "finisher": function() {
+                    self.plGrid.hideLoadProgressIndicator();
+                },
+                "adder": function(pl) {
+                    self.plCache[pl.id()] = pl;
+                    self.plGrid.store.newItem(acqpl.toStoreItem(pl));
+                },
+                "interface": self.plGrid
             },
-            "adder": function(inv) {
-                self.invCache[inv.id()] = inv;
-                self.invGrid.store.newItem(acqinv.toStoreItem(inv));
+            "invoice": {
+                "search_options": {
+                    "no_flesh_misc": true
+                },
+                "finisher": function() {
+                    self.invGrid.hideLoadProgressIndicator();
+                },
+                "revealer": function() {
+                    self.invGrid.resetStore();
+                    self.invCache = {};
+                },
+                "adder": function(inv) {
+                    self.invCache[inv.id()] = inv;
+                    self.invGrid.store.newItem(acqinv.toStoreItem(inv));
+                },
+                "interface": self.invGrid
             },
-            "interface": self.invGrid
-        },
-        "no_results": {
-            "revealer": function() { alert(localeStrings.NO_RESULTS); }
+            "no_results": {
+                "revealer": function() { alert(localeStrings.NO_RESULTS); }
+            }
         }
     };
 
@@ -788,10 +796,23 @@ function ResultManager(liPager, poGrid, plGrid, invGrid) {
     };
 
     this.go = function(search_object) {
+
+        if (window.unifiedSearchExternalMode) {
+            // assume for now that external mode implies inline results display
+            
+            uriManager = uriManager || new URIManager();
+            uriManager.search_object = search_object;
+            uriManager.result_type = dojo.byId("acq-unified-result-type").getValue();
+            uriManager.conjunction = dojo.byId("acq-unified-conjunction").getValue();
+            this.search(uriManager, termManager);
+
+        } else {
+
         location.href = oilsBasePath + "/acq/search/unified?" +
             "so=" + base64Encode(search_object) +
             "&rt=" + dojo.byId("acq-unified-result-type").getValue() +
             "&c=" + dojo.byId("acq-unified-conjunction").getValue();
+        }
     };
 
     this.search = function(uriManager, termManager) {
@@ -928,7 +949,12 @@ function URIManager() {
 /* onload */
 openils.Util.addOnLoad(
     function() {
+
+        // onload handled by external user
+        if (window.unifiedSearchExternalMode) return;
+
         termManager = new TermManager();
+
         resultManager = new ResultManager(
             new LiTablePager(null, new AcqLiTable()),
             dijit.byId("acq-unified-po-grid"),
diff --git a/docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt b/docs/RELEASE_NOTES_NEXT/acq-invoice-li-search.txt
new file mode 100644 (file)
index 0000000..3ef304d
--- /dev/null
@@ -0,0 +1,22 @@
+ACQ Invoice Inline Lineitem Search and Add
+------------------------------------------
+
+The Invoice UI is how composed of two tabs, the main invoice tab and a new Search tab.  The search tab consists of a subset of the Acquisitions unified search interface.  The goal is to allow users to search for lineitems to invoice.  Search results may be added directly to the growing invoice.  A number of small usability features are included.
+
+Features
+~~~~~~~~
+
+    * Option (default) to limit searches to invoiceable items.  
+        ** These are lineitems that are not cancelled, have at least one invoiceable copy, linked to a PO whose provider matches that of the current invoice, and are not already linked to the current invoice.
+    * Search defaults to last-run search (on workstation).
+    * New Lineitem Detail filter options
+    * Sort searches by lineitem number (default) and title.
+    * There is a new Expected Cost field which includes both the total invoiced cost plus the anticipated cost of lineitems as they are added.
+    * New Price per Copy field
+    * Lineitem count field
+    * Show / Hide Invoice details button.  Details are displayed by default, but hidden when the user enters the search tab.  From there it remains hidden until manually shown (or a new invoice is opened).
+    * A new "Save & Clear" button which saves the current invoice then clears the invoice display to create a new invoice.
+    * Provider, shipper, and receiver fields are auto-populated from the first-added invoice data (when not already set).
+    * Totals are now read-only, since they are derived from existing data (and are informational only).
+
+