Turn away! Avert your eyes!
authormiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 13 Jan 2010 16:35:03 +0000 (16:35 +0000)
committermiker <miker@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 13 Jan 2010 16:35:03 +0000 (16:35 +0000)
Herein lies the initial 1.6 backport of changes* implementing the booking
module for Evergreen.  1.6.1, we look forward to you!

*) Changesets included: 14921, 14925, 15019, 15068, 15071, 15072, 15076, 15077, 15092, 15096, 15099, 15100, 15103, 15104, 15108, 15110, 15113, 15128, 15133, 15161, 15164, 15188, 15207, 15211, 15215, 15223, 15224, 15228, 15236, 15241, 15247, 15264, 15285, 15288, 15289, 15309

git-svn-id: svn://svn.open-ils.org/ILS/branches/rel_1_6@15312 dcc99617-32d9-48b4-a31d-7c20da2025e4

84 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/examples/opensrf.xml.example
Open-ILS/examples/opensrf_core.xml.example
Open-ILS/src/extras/ils_events.xml
Open-ILS/src/perlmods/OpenILS/Application/AppUtils.pm
Open-ILS/src/perlmods/OpenILS/Application/Booking.pm [new file with mode: 0644]
Open-ILS/src/perlmods/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm
Open-ILS/src/perlmods/OpenILS/Application/Collections.pm
Open-ILS/src/perlmods/OpenILS/Application/CreditCard.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/action.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm [new file with mode: 0644]
Open-ILS/src/perlmods/OpenILS/Application/Storage/Driver/Pg/dbi.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/money.pm
Open-ILS/src/perlmods/OpenILS/Const.pm
Open-ILS/src/perlmods/OpenILS/Utils/CStoreEditor.pm
Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/095.schema.booking.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/500.view.cross-schema.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/build-db.sh
Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql [new file with mode: 0644]
Open-ILS/src/support-scripts/test-scripts/collections.pl
Open-ILS/web/css/skin/default/booking.css [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/booking/nls/capture.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/booking/nls/reservation.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/capture.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/common.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/pickup.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/populator.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/pull_list.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/reservation.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/booking/return.js [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/skin/default/js/myopac.js
Open-ILS/web/templates/default/booking/capture.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/booking/pickup.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/booking/pull_list.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/booking/reservation.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/booking/return.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/resource.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 [new file with mode: 0644]
Open-ILS/xul/staff_client/chrome/content/main/constants.js
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_overlay.xul
Open-ILS/xul/staff_client/chrome/content/util/functional.js
Open-ILS/xul/staff_client/chrome/locale/en-US/offline.properties
Open-ILS/xul/staff_client/server/admin/index.xhtml
Open-ILS/xul/staff_client/server/cat/copy_browser.js
Open-ILS/xul/staff_client/server/cat/copy_browser.xul
Open-ILS/xul/staff_client/server/cat/util.js
Open-ILS/xul/staff_client/server/circ/copy_status.js
Open-ILS/xul/staff_client/server/circ/copy_status.xul
Open-ILS/xul/staff_client/server/circ/copy_status_overlay.xul
Open-ILS/xul/staff_client/server/locale/en-US/cat.properties
Open-ILS/xul/staff_client/server/locale/en-US/patron.properties
Open-ILS/xul/staff_client/server/patron/bills.js
Open-ILS/xul/staff_client/server/patron/display.js
Open-ILS/xul/staff_client/server/patron/display.xul
Open-ILS/xul/staff_client/server/patron/display_horiz.xul
Open-ILS/xul/staff_client/server/patron/display_horiz_overlay.xul
Open-ILS/xul/staff_client/server/patron/display_overlay.xul

index aa28b28..0a96b06 100644 (file)
@@ -1649,6 +1649,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Open Billable Transactions" name="open_billable_transactions_summary" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Checkins" name="checkins" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Circulations Performed as Staff" name="performed_circulations" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Reservations" name="reservations" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="demographic" reltype="might_have" key="id" map="" class="rud"/>
@@ -1678,6 +1679,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link reporter:label="Check-ins Performed as Staff" field="checkins" reltype="has_many" key="checkin_staff" map="" class="circ"/>
                        <link field="cards" reltype="has_many" key="usr" map="" class="ac"/>
                        <link reporter:label="Circulations Performed as Staff" field="performed_circulations" reltype="has_many" key="circ_staff" map="" class="circ"/>
+                       <link field="reservations" reltype="has_many" key="usr" map="" class="bresv"/>
                </links>
        </class>
        <class id="aous" controller="open-ils.cstore" oils_obj:fieldmapper="actor::org_unit_setting" oils_persist:tablename="actor.org_unit_setting" reporter:label="Organizational Unit Setting">
@@ -2208,6 +2210,221 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="copy_bib_record" reltype="has_a" key="id" map="" class="bre"/>
                </links>
        </class>
+
+       <class id="brt" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource_type" oils_persist:tablename="booking.resource_type" reporter:label="Resource Type">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.resource_type_id_seq">
+                       <field reporter:label="Resource Type ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Resource Type Name" name="name" reporter:datatype="text"/>
+                       <field reporter:label="Fine Interval" name="fine_interval" reporter:datatype="interval"/>
+                       <field reporter:label="Fine Amount" name="fine_amount" reporter:datatype="money"/>
+                       <field reporter:label="Max Fine Amount" name="max_fine" reporter:datatype="money"/>
+                       <field reporter:label="Owning Library" name="owner" reporter:datatype="org_unit"/>
+                       <field reporter:label="Catalog Item" name="catalog_item" reporter:datatype="bool"/>
+                       <field reporter:label="Bibliographic Record" name="record" reporter:datatype="link"/>
+                       <field reporter:label="Transferable" name="transferable" reporter:datatype="bool"/>
+                       <field reporter:label="Inter-booking and Inter-circulation Interval" name="elbow_room" reporter:datatype="interval"/>
+                       <field reporter:label="Resources" name="resources" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resource Attributes" name="resource_attrs" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Target Resource Types" name="tgt_rsrc_types" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="record" reltype="has_a" key="id" map="" class="bre"/>
+                       <link field="resources" reltype="has_many" key="type" map="" class="brsrc"/>
+                       <link field="resource_attrs" reltype="has_many" key="type" map="" class="bra"/>
+                       <link field="tgt_rsrc_types" reltype="has_many" key="type" map="" class="bresv"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESOURCE_TYPE" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESOURCE_TYPE" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESOURCE_TYPE" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+
+       <class id="brsrc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource" oils_persist:tablename="booking.resource" reporter:label="Resource">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.resource_id_seq">
+                       <field reporter:label="Resource ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Owning Library" name="owner" reporter:datatype="org_unit"/>
+                       <field reporter:label="Resource Type" name="type" reporter:datatype="link"/>
+                       <field reporter:label="Overbook" name="overbook" reporter:datatype="bool"/>
+                       <field reporter:label="Barcode" name="barcode" reporter:datatype="text"/>
+                       <field reporter:label="Is Deposit Required" name="deposit" reporter:datatype="bool"/>
+                       <field reporter:label="Deposit Amount" name="deposit_amount" reporter:datatype="money"/>
+                       <field reporter:label="User Fee" name="user_fee" reporter:datatype="money"/>
+                       <field reporter:label="Resource Attribute Maps" name="attr_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Reservation Target Resources" name="tgt_rsrcs" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Reservation Current Resources" name="curr_rsrcs" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Catalog Item" name="catalog_item" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="type" reltype="has_a" key="id" map="" class="brt"/>
+                       <link field="attr_maps" reltype="has_many" key="resource" map="" class="bram"/>
+                       <link field="tgt_rsrcs" reltype="has_many" key="targeted_resource" map="" class="bresv"/>
+                       <link field="curr_rsrcs" reltype="has_many" key="current_resource" map="" class="bresv"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESOURCE" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESOURCE" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESOURCE" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+       
+       <class id="bra" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource_attr" oils_persist:tablename="booking.resource_attr" reporter:label="Resource Attribute">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.resource_attr_id_seq">
+                       <field reporter:label="Resource Attribute ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Owning Library" name="owner" reporter:datatype="org_unit"/>
+                       <field reporter:label="Resource Attribute Name" name="name" reporter:datatype="text"/>
+                       <field reporter:label="Resource Type" name="resource_type" reporter:datatype="link"/>
+                       <field reporter:label="Is Required" name="required" reporter:datatype="bool"/>
+                       <field reporter:label="Valid Values" name="valid_values" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resource Attribute Maps" name="attr_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="resource_type" reltype="has_a" key="id" map="" class="brt"/>
+                       <link field="valid_values" reltype="has_many" key="attr" map="" class="brav"/>
+                       <link field="attr_maps" reltype="has_many" key="attr" map="" class="bram"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESOURCE_ATTR" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESOURCE_ATTR" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESOURCE_ATTR" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+       
+       <class id="brav" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource_attr_value" oils_persist:tablename="booking.resource_attr_value" reporter:label="Resource Attribute Value">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.resource_attr_value_id_seq">
+                       <field reporter:label="Resource Attribute Value ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Owning Library" name="owner" reporter:datatype="org_unit"/>
+                       <field reporter:label="Resource Attribute" name="attr" reporter:datatype="link"/>
+                       <field reporter:label="Valid Value" name="valid_value" reporter:datatype="text"/>
+                       <field reporter:label="Resource Attribute Maps" name="attr_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resource Attribute Value Maps" name="attr_val_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="attr" reltype="has_a" key="id" map="" class="bra"/>
+                       <link field="attr_maps" reltype="has_many" key="id" map="" class="bram"/>
+                       <link field="attr_val_maps" reltype="has_many" key="attr_value" map="" class="bravm"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESOURCE_ATTR_VALUE" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESOURCE_ATTR_VALUE" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESOURCE_ATTR_VALUE" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+       
+       <class id="bram" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::resource_attr_map" oils_persist:tablename="booking.resource_attr_map" reporter:label="Resource Attribute Map">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.resource_attr_map_id_seq">
+                       <field reporter:label="Resource Attribute Map ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Resource" name="resource" reporter:datatype="link"/>
+                       <field reporter:label="Resource Attribute" name="resource_attr" reporter:datatype="link"/>
+                       <field reporter:label="Attribute Value" name="value" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="resource" reltype="has_a" key="id" map="" class="brsrc"/>
+                       <link field="resource_attr" reltype="has_a" key="id" map="" class="bra"/>
+                       <link field="value" reltype="has_a" key="id" map="" class="brav"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESOURCE_ATTR_MAP" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESOURCE_ATTR_MAP" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESOURCE_ATTR_MAP" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+       
+       <class id="bresv" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::reservation" oils_persist:tablename="booking.reservation" reporter:label="Reservation">
+               <fields oils_persist:primary="id" oils_persist:sequence="money.billable_xact_id_seq">
+                       <field reporter:label="Transaction ID" name="id" reporter:datatype="id" />
+                       <field reporter:label="User" name="usr" reporter:datatype="link"/>
+                       <field reporter:label="Transaction Finish Date/Time" name="xact_finish" reporter:datatype="timestamp"/>
+                       <field reporter:label="Transaction Start Date/Time" name="xact_start" reporter:datatype="timestamp"/>
+                       <field reporter:label="Unrecovered Debt" name="unrecovered" reporter:datatype="bool"/>
+                       <field reporter:label="Billing Line Items" name="billings" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Payment Line Items" name="payments" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Billing Totals" name="billing_total" oils_persist:virtual="true" reporter:datatype="money"/>
+                       <field reporter:label="Payment Totals" name="payment_total" oils_persist:virtual="true" reporter:datatype="money"/>
+                       <field reporter:label="Payment Summary" name="summary" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Request Time" name="request_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Start Time" name="start_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="End Time" name="end_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Capture Time" name="capture_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Cancel Time" name="cancel_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Pickup Time" name="pickup_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Return Time" name="return_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Booking Interval" name="booking_interval" reporter:datatype="interval"/>
+                       <field reporter:label="Fine Interval" name="fine_interval" reporter:datatype="interval"/>
+                       <field reporter:label="Fine Amount" name="fine_amount" reporter:datatype="money"/>
+                       <field reporter:label="Max Fine Amount" name="max_fine" reporter:datatype="money"/>
+                       <field reporter:label="Target Resource Type" name="target_resource_type" reporter:datatype="link"/>
+                       <field reporter:label="Target Resource" name="target_resource" reporter:datatype="link"/>
+                       <field reporter:label="Current Resource" name="current_resource" reporter:datatype="link"/>
+                       <field reporter:label="Request Library" name="request_lib" reporter:datatype="link"/>
+                       <field reporter:label="Pickup Library" name="pickup_lib" reporter:datatype="link"/>
+                       <field reporter:label="Capture Staff" name="capture_staff" reporter:datatype="link"/>
+                       <field reporter:label="Attribute Value Maps" name="attr_val_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="usr" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="payments" reltype="has_many" key="xact" map="" class="mp"/>
+                       <link field="billings" reltype="has_many" key="xact" map="" class="mb"/>
+                       <link field="billing_total" reltype="might_have" key="xact" map="" class="rxbt"/>
+                       <link field="payment_total" reltype="might_have" key="xact" map="" class="rxpt"/>
+                       <link field="summary" reltype="might_have" key="id" map="" class="mbts"/>
+                       <link field="target_resource_type" reltype="has_a" key="id" map="" class="brt"/>
+                       <link field="target_resource" reltype="has_a" key="id" map="" class="brsrc"/>
+                       <link field="current_resource" reltype="has_a" key="id" map="" class="brsrc"/>
+                       <link field="request_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="pickup_lib" reltype="might_have" key="id" map="" class="aou"/>
+                       <link field="capture_staff" reltype="might_have" key="id" map="" class="au"/>
+                       <link field="attr_val_maps" reltype="has_many" key="reservation" map="" class="bravm"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESERVATION" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESERVATION" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESERVATION" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+       
+       <class id="bravm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="booking::reservation_attr_value_map" oils_persist:tablename="booking.reservation_attr_value_map" reporter:label="Reservation Attribute Value Map">
+               <fields oils_persist:primary="id" oils_persist:sequence="booking.reservation_attr_value_map_id_seq">
+                       <field reporter:label="Reservation Attribute Value Map" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Reservation" name="reservation" reporter:datatype="link"/>
+                       <field reporter:label="Attribute Map" name="attr_value" reporter:datatype="link"/>
+               </fields>
+               <links>
+                       <link field="reservation" reltype="has_a" key="id" map="" class="bresv"/>
+                       <link field="attr_value" reltype="has_a" key="id" map="" class="brav"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_BOOKING_RESERVATION_ATTR_MAP" global_required='true'/>
+                               <retrieve />
+                               <update permission="ADMIN_BOOKING_RESERVATION_ATTR_MAP" global_required='true'/>
+                               <delete permission="ADMIN_BOOKING_RESERVATION_ATTR_MAP" global_required='true'/>
+                       </actions>
+               </permacrud>
+       </class>
+
        <class id="ccnbi" controller="open-ils.cstore" oils_obj:fieldmapper="container::call_number_bucket_item" oils_persist:tablename="container.call_number_bucket_item" reporter:label="Call Number Bucket Item">
                <fields oils_persist:primary="id" oils_persist:sequence="container.call_number_bucket_item_id_seq">
                        <field name="bucket" reporter:datatype="link"/>
@@ -2676,6 +2893,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Addresses" name="addresses" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Checkins" name="checkins" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="Workstations" name="workstations" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Reservation Requests" name="resv_requests" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Reservation Pickups" name="resv_pickups" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resource Types" name="rsrc_types" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resources" name="resources" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Resource Attributes" name="rsrc_attrs" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Attribute Values" name="attr_vals" oils_persist:virtual="true" reporter:datatype="link"/>
+
                </fields>
                <links>
                        <link field="billing_address" reltype="has_a" key="id" map="" class="aoa"/>
@@ -2694,6 +2918,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="workstations" reltype="has_many" key="owning_lib" map="" class="aws"/>
                        <link field="distribution_formulas" reltype="has_many" key="owner" map="" class="acqdf"/>
                        <link field="distribution_formula_entries" reltype="has_many" key="owning_lib" map="" class="acqdfe"/>
+                       <link field="resv_requests" reltype="has_many" key="request_lib" map="" class="bresv"/>
+                       <link field="resv_pickups" reltype="has_many" key="pickup_lib" map="" class="bresv"/>
+                       <link field="rsrc_types" reltype="has_many" key="owner" map="" class="brt"/>
+                       <link field="resources" reltype="has_many" key="owner" map="" class="brsrc"/>
+                       <link field="rsrc_attrs" reltype="has_many" key="owner" map="" class="bra"/>
+                       <link field="attr_vals" reltype="has_many" key="owner" map="" class="brav"/>
                </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -3616,6 +3846,41 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
        </class>
 
 
+       <class id="artc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::reservation_transit_copy" oils_persist:tablename="action.reservation_transit_copy" reporter:core="true" reporter:label="Reservation Transit">
+               <fields oils_persist:primary="id" oils_persist:sequence="action.transit_copy_id_seq">
+                       <field reporter:label="Copy Status at Transit" name="copy_status" reporter:datatype="link"/>
+                       <field reporter:label="Destination Library" name="dest" reporter:datatype="org_unit"/>
+                       <field reporter:label="Receive Date/Time" name="dest_recv_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Reservation requiring Transit" name="reservation" reporter:datatype="link"/>
+                       <field reporter:label="Transit ID" name="id" reporter:datatype="id" />
+                       <field reporter:label="Is Persistent?" name="persistant_transfer" reporter:datatype="bool"/>
+                       <field reporter:label="Previous Stop" name="prev_hop" reporter:datatype="link"/>
+                       <field reporter:label="Sending Library" name="source" reporter:datatype="org_unit"/>
+                       <field reporter:label="Send Date/Time" name="source_send_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Transited Copy" name="target_copy" reporter:datatype="link"/>
+                       <field reporter:label="Base Transit" name="transit_copy" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Prev Destination Library" name="prev_dest" reporter:datatype="org_unit"/>
+               </fields>
+               <links>
+                       <link field="transit_copy" reltype="might_have" key="id" map="" class="atc"/>
+                       <link field="target_copy" reltype="has_a" key="id" map="" class="brsrc"/>
+                       <link field="source" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="copy_status" reltype="has_a" key="id" map="" class="ccs"/>
+                       <link field="dest" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="prev_dest" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="reservation" reltype="has_a" key="id" map="" class="bresv"/>
+               </links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="TRANSIT_COPY">
+                    <context link="target_copy" field="owner"/>
+                </create>
+                <retrieve/>
+                <update permission="UPDATE_TRANSIT" context_field="dest source"/>
+                <delete permission="DELETE_TRANSIT" context_field="dest source"/>
+            </actions>
+        </permacrud>
+       </class>
        <class id="ahtc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::hold_transit_copy" oils_persist:tablename="action.hold_transit_copy" reporter:core="true" reporter:label="Hold Transit">
                <fields oils_persist:primary="id" oils_persist:sequence="action.transit_copy_id_seq">
                        <field reporter:label="Copy Status at Transit" name="copy_status" reporter:datatype="link"/>
index 6d58247..552ec32 100644 (file)
@@ -430,6 +430,28 @@ vim:et:ts=4:sw=4:
                 </unix_config>
             </open-ils.actor>
 
+            <open-ils.booking>
+                <keepalive>5</keepalive>
+                <stateless>1</stateless>
+                <language>perl</language>
+                <implementation>OpenILS::Application::Booking</implementation>
+                <max_requests>199</max_requests>
+                <unix_config>
+                    <unix_sock>open-ils.booking_unix.sock</unix_sock>
+                    <unix_pid>open-ils.booking_unix.pid</unix_pid>
+                    <max_requests>1000</max_requests>
+                    <unix_log>open-ils.booking_unix.log</unix_log>
+                    <min_children>1</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>1</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                    <marctemplates>
+                        <K_book>LOCALSTATEDIR/templates/marc/k_book.xml</K_book>
+                    </marctemplates>
+                </app_settings>
+            </open-ils.booking>
 
             <open-ils.cat>
                 <keepalive>5</keepalive>
@@ -946,6 +968,7 @@ vim:et:ts=4:sw=4:
                 <appname>opensrf.math</appname> 
                 <appname>opensrf.dbmath</appname> 
                 <appname>open-ils.acq</appname> 
+                <appname>open-ils.booking</appname>
                 <appname>open-ils.cat</appname> 
                 <appname>open-ils.supercat</appname> 
                 <appname>open-ils.search</appname> 
index 695ab4a..3097ce0 100644 (file)
@@ -22,6 +22,7 @@ Example OpenSRF bootstrap configuration file for Evergreen
           <service>open-ils.actor</service>
           <service>open-ils.acq</service>
           <service>open-ils.auth</service>
+          <service>open-ils.booking</service>
           <service>open-ils.cat</service>
           <service>open-ils.circ</service>
           <service>open-ils.collections</service>
index 1d31af5..47b43b7 100644 (file)
        <event code='7019' textcode='HOLD_CAPTURE_DELAYED'>
                <desc xml:lang="en-US">Hold capture was delayed for this item</desc>
        </event>
+       <event code='7020' textcode='COPY_RESERVED'>
+               <desc xml:lang="en-US">Item reserved for booking request</desc>
+       </event>
+       <event code='7021' textcode='RESERVATION_NOT_FOUND'>
+               <desc xml:lang="en-US">Booking reservation not found</desc>
+       </event>
+       <event code='7022' textcode='RESERVATION_CAPTURE_FAILED'>
+               <desc xml:lang="en-US">Booking reservation capture failed</desc>
+       </event>
+       <event code='7023' textcode='RESERVATION_BAD_PARAMS'>
+               <desc xml:lang="en-US">Provided parameters describe unacceptable reservation.</desc>
+       </event>
 
 
 
index 6281e4e..496222c 100644 (file)
@@ -811,6 +811,64 @@ sub DB_UPDATE_FAILED {
                payload => ($payload) ? $payload : undef ); 
 }
 
+sub fetch_booking_reservation {
+       my( $self, $id ) = @_;
+       my( $res, $evt );
+
+       $res = $self->simplereq(
+               'open-ils.cstore', 
+               'open-ils.cstore.direct.booking.reservation.retrieve', $id
+       );
+
+       # simplereq doesn't know how to flesh so ...
+       if ($res) {
+               $res->usr(
+                       $self->simplereq(
+                               'open-ils.cstore', 
+                               'open-ils.cstore.direct.actor.user.retrieve', $res->usr
+                       )
+               );
+
+               $res->target_resource_type(
+                       $self->simplereq(
+                               'open-ils.cstore', 
+                               'open-ils.cstore.direct.booking.resource_type.retrieve', $res->target_resource_type
+                       )
+               );
+
+               if ($res->current_resource) {
+                       $res->current_resource(
+                               $self->simplereq(
+                                       'open-ils.cstore', 
+                                       'open-ils.cstore.direct.booking.resource.retrieve', $res->current_resource
+                               )
+                       );
+
+                       if ($self->is_true( $res->target_resource_type->catalog_item )) {
+                               $res->current_resource->catalog_item( $self->fetch_copy_by_barcode( $res->current_resource->barcode ) );
+                       }
+               }
+
+               if ($res->target_resource) {
+                       $res->target_resource(
+                               $self->simplereq(
+                                       'open-ils.cstore', 
+                                       'open-ils.cstore.direct.booking.resource.retrieve', $res->target_resource
+                               )
+                       );
+
+                       if ($self->is_true( $res->target_resource_type->catalog_item )) {
+                               $res->target_resource->catalog_item( $self->fetch_copy_by_barcode( $res->target_resource->barcode ) );
+                       }
+               }
+
+       } else {
+               $evt = OpenILS::Event->new('RESERVATION_NOT_FOUND');
+       }
+
+       return ($res, $evt);
+}
+
 sub fetch_circ_duration_by_name {
        my( $self, $name ) = @_;
        my( $dur, $evt );
@@ -951,6 +1009,16 @@ sub unflesh_copy {
        return $copy;
 }
 
+sub unflesh_reservation {
+       my( $self, $reservation ) = @_;
+       return undef unless $reservation;
+       $reservation->usr( $reservation->usr->id ) if ref($reservation->usr);
+       $reservation->target_resource_type( $reservation->target_resource_type->id ) if ref($reservation->target_resource_type);
+       $reservation->target_resource( $reservation->target_resource->id ) if ref($reservation->target_resource);
+       $reservation->current_resource( $reservation->current_resource->id ) if ref($reservation->current_resource);
+       return $reservation;
+}
+
 # un-fleshes a copy and updates it in the DB
 # returns a DB_UPDATE_FAILED event on error
 # returns undef on success
@@ -979,6 +1047,29 @@ sub update_copy {
        return undef;
 }
 
+sub update_reservation {
+       my( $self, %params ) = @_;
+
+       my $reservation = $params{reservation}  || die "update_reservation(): reservation required";
+       my $editor              = $params{editor} || die "update_reservation(): copy editor required";
+       my $session             = $params{session};
+
+       $logger->debug("Updating copy in the database: " . $reservation->id);
+
+       $self->unflesh_reservation($reservation);
+
+       my $s;
+       my $meth = 'open-ils.cstore.direct.booking.reservation.update';
+
+       $s = $session->request( $meth, $reservation )->gather(1) if $session;
+       $s = $self->cstorereq( $meth, $reservation ) unless $session;
+
+       $logger->debug("Update of copy ".$reservation->id." returned: $s");
+
+       return $self->DB_UPDATE_FAILED($reservation) unless $s;
+       return undef;
+}
+
 sub fetch_billable_xact {
        my( $self, $id ) = @_;
        my($xact, $evt);
diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm b/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm
new file mode 100644 (file)
index 0000000..dc6ab3b
--- /dev/null
@@ -0,0 +1,1148 @@
+package OpenILS::Application::Booking;
+
+use strict;
+use warnings;
+
+use POSIX qw/strftime/;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = "OpenILS::Application::AppUtils";
+
+use OpenSRF::Utils::Logger qw/$logger/;
+
+sub prepare_new_brt {
+    my ($record_id, $owning_lib, $mvr) = @_;
+
+    my $brt = new Fieldmapper::booking::resource_type;
+    $brt->isnew(1);
+    $brt->name($mvr->title);
+    $brt->record($record_id);
+    $brt->catalog_item('t');
+    $brt->transferable('t');
+    $brt->owner($owning_lib);
+
+    return $brt;
+}
+
+sub get_existing_brt {
+    my ($e, $record_id, $owning_lib, $mvr) = @_;
+    my $results = $e->search_booking_resource_type(
+        {name => $mvr->title, owner => $owning_lib, record => $record_id}
+    );
+
+    return $results->[0] if scalar(@$results) > 0;
+    return undef;
+}
+
+sub get_mvr {
+    return $U->simplereq(
+        'open-ils.search',
+        'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
+        shift # record id
+    );
+}
+
+sub get_unique_owning_libs {
+    my %hash = ();
+    $hash{$_->call_number->owning_lib} = 1 foreach (@_);    # @_ are copies
+    return keys %hash;
+}
+
+sub fetch_copies_by_ids {
+    my ($e, $copy_ids) = @_;
+    my $results = $e->search_asset_copy([
+        {id => $copy_ids},
+        {flesh => 1, flesh_fields => {acp => ['call_number']}}
+    ]);
+    return $results if ref($results) eq 'ARRAY';
+    return [];
+}
+
+sub get_single_record_id {
+    my $record_id = undef;
+    foreach (@_) {  # @_ are copies
+        return undef if
+            (defined $record_id && $record_id != $_->call_number->record);
+        $record_id = $_->call_number->record;
+    }
+    return $record_id;
+}
+
+# This function generates the correct json_query clause for determining
+# whether two given ranges overlap.  Each range is composed of a start
+# and an end point.  All four points should be the same type (could be int,
+# date, time, timestamp, or perhaps other types).
+#
+# The first range (or the first two points) should be specified as
+# literal values.  The second range (or the last two points) should be
+# specified as the names of columns, the values of which in a given row
+# will constitute the second range in the comparison.
+#
+# ALSO: PostgreSQL includes an OVERLAPS operator which provides the same
+# functionality in a much more concise way, but json_query does not (yet).
+sub json_query_ranges_overlap {
+    +{ '-or' => [
+        { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<',  $_[1]}}]},
+        { '-and' => [{$_[3] => {'>',  $_[0]}}, {$_[3] => {'<',  $_[1]}}]},
+        { '-and' => { $_[3] => {'>',  $_[0]},   $_[2] => {'<=', $_[0]}}},
+        { '-and' => { $_[3] => {'>',  $_[1]},   $_[2] => {'<',  $_[1]}}},
+    ]};
+}
+
+sub create_brt_and_brsrc {
+    my ($self, $conn, $authtoken, $copy_ids) = @_;
+    my (@created_brt, @created_brsrc);
+    my %brt_table = ();
+
+    my $e = new_editor(xact => 1, authtoken => $authtoken);
+    return $e->die_event unless $e->checkauth;
+
+    my @copies = @{fetch_copies_by_ids($e, $copy_ids)};
+    my $record_id = get_single_record_id(@copies) or return $e->die_event;
+    my $mvr = get_mvr($record_id) or return $e->die_event;
+
+    foreach (get_unique_owning_libs(@copies)) {
+        $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) ||
+            prepare_new_brt($record_id, $_, $mvr);
+    }
+
+    while (my ($owning_lib, $brt) = each %brt_table) {
+        my $pre_existing = 1;
+        if ($brt->isnew) {
+            if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) {
+                $pre_existing = 0;
+                return $e->die_event unless (
+                    #    v-- Important: assignment modifies original hash
+                    $brt = $e->create_booking_resource_type($brt)
+                );
+            }
+        }
+        push @created_brt, [$brt->id, $brt->record, $pre_existing];
+    }
+
+    foreach (@copies) {
+        if ($e->allowed(
+            'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib
+        )) {
+            # This block needs to disregard any cstore failures and just
+            # return what results it can.
+            my $brsrc = new Fieldmapper::booking::resource;
+            $brsrc->isnew(1);
+            $brsrc->type($brt_table{$_->call_number->owning_lib}->id);
+            $brsrc->owner($_->call_number->owning_lib);
+            $brsrc->barcode($_->barcode);
+
+            $e->set_savepoint("alpha");
+            my $pre_existing = 0;
+            my $usable_result = undef;
+            if (!($usable_result = $e->create_booking_resource($brsrc))) {
+                $e->rollback_savepoint("alpha");
+                if (($usable_result = $e->search_booking_resource(
+                    +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ }
+                ))) {
+                    $usable_result = $usable_result->[0];
+                    $pre_existing = 1;
+                } else {
+                    # So we failed to create a booking resource for this copy.
+                    # For now, let's just keep going.  If the calling app wants
+                    # to consider this an error, it can notice the absence
+                    # of a booking resource for the copy in the returned
+                    # results.
+                    $logger->warn(
+                        "Couldn't create or find brsrc for acp #" .  $_->id
+                    );
+                }
+            } else {
+                $e->release_savepoint("alpha");
+            }
+
+            if ($usable_result) {
+                push @created_brsrc,
+                    [$usable_result->id, $_->id, $pre_existing];
+            }
+        }
+    }
+
+    $e->commit and
+        return {brt => \@created_brt, brsrc => \@created_brsrc} or
+        return $e->die_event;
+}
+__PACKAGE__->register_method(
+    method   => "create_brt_and_brsrc",
+    api_name => "open-ils.booking.resources.create_from_copies",
+    signature => {
+        params => [
+            {type => 'string', desc => 'Authentication token'},
+            {type => 'array', desc => 'Copy IDs'},
+        ],
+        return => { desc => "A two-element hash. The 'brt' element " .
+            "is a list of created booking resource types described by " .
+            "3-tuples (id, copy id, was pre-existing).  The 'brsrc' " .
+            "element is a similar list of created booking resources " .
+            "described by (id, record id, was pre-existing) 3-tuples."}
+    }
+);
+
+
+sub create_bresv {
+    my ($self, $client, $authtoken,
+        $target_user_barcode, $datetime_range, $pickup_lib,
+        $brt, $brsrc_list, $attr_values) = @_;
+
+    $brsrc_list = [ undef ] if not defined $brsrc_list;
+    return undef if scalar(@$brsrc_list) < 1; # Empty list not ok.
+
+    my $e = new_editor(xact => 1, authtoken => $authtoken);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
+
+    my $usr = $U->fetch_user_by_barcode($target_user_barcode);
+    return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
+
+    my $results = [];
+    foreach my $brsrc (@$brsrc_list) {
+        my $bresv = new Fieldmapper::booking::reservation;
+        $bresv->usr($usr->id);
+        $bresv->request_lib($e->requestor->ws_ou);
+        $bresv->pickup_lib($pickup_lib);
+        $bresv->start_time($datetime_range->[0]);
+        $bresv->end_time($datetime_range->[1]);
+
+        # A little sanity checking: don't agree to put a reservation on a
+        # brsrc and a brt when they don't match.  In fact, bomb out of
+        # this transaction entirely.
+        if ($brsrc) {
+            my $brsrc_itself = $e->retrieve_booking_resource([
+                $brsrc, {
+                    "flesh" => 1,
+                    "flesh_fields" => {"brsrc" => ["type"]}
+                }
+            ]);
+
+            if (not $brsrc_itself) {
+                my $ev = new OpenILS::Event(
+                    "RESERVATION_BAD_PARAMS",
+                    desc => "brsrc $brsrc doesn't exist"
+                );
+                $e->disconnect;
+                return $ev;
+            }
+            elsif ($brsrc_itself->type->id != $brt) {
+                my $ev = new OpenILS::Event(
+                    "RESERVATION_BAD_PARAMS",
+                    desc => "brsrc $brsrc doesn't match given brt $brt"
+                );
+                $e->disconnect;
+                return $ev;
+            }
+
+            # Also bail if the user is trying to create a reservation at
+            # a pickup lib to which our resource won't go.
+            if (
+                $brsrc_itself->owner != $pickup_lib and
+                    not $brsrc_itself->type->transferable
+            ) {
+                my $ev = new OpenILS::Event(
+                    "RESERVATION_BAD_PARAMS",
+                    desc => "brsrc $brsrc doesn't belong to $pickup_lib and " .
+                        "is not transferable"
+                );
+                $e->disconnect;
+                return $ev;
+            }
+        }
+        $bresv->target_resource($brsrc);    # undef is ok here
+        $bresv->target_resource_type($brt);
+
+        ($bresv = $e->create_booking_reservation($bresv)) or
+            return $e->die_event;
+
+        # We could/should do some sanity checking on this too: namely, on
+        # whether the attribute values given actually apply to the relevant
+        # brt.  Not seeing any grievous side effects of not checking, though.
+        my @bravm = ();
+        foreach my $value (@$attr_values) {
+            my $bravm = new Fieldmapper::booking::reservation_attr_value_map;
+            $bravm->reservation($bresv->id);
+            $bravm->attr_value($value);
+            $bravm = $e->create_booking_reservation_attr_value_map($bravm) or
+                return $e->die_event;
+            push @bravm, $bravm;
+        }
+        push @$results, {
+            "bresv" => $bresv->id,
+            "bravm" => \@bravm,
+        };
+    }
+
+    $e->commit or return $e->die_event;
+
+    # Targeting must be tacked on _after_ committing the transaction where the
+    # reservations are actually created.
+    foreach (@$results) {
+        $_->{"targeting"} = $U->storagereq(
+            "open-ils.storage.booking.reservation.resource_targeter",
+            $_->{"bresv"}
+        )->[0];
+    }
+    return $results;
+}
+__PACKAGE__->register_method(
+    method   => "create_bresv",
+    api_name => "open-ils.booking.reservations.create",
+    signature => {
+        params => [
+            {type => 'string', desc => 'Authentication token'},
+            {type => 'string', desc => 'Barcode of user for whom to reserve'},
+            {type => 'array', desc => 'Two elements: start and end timestamp'},
+            {type => 'int', desc => 'Desired reservation pickup lib'},
+            {type => 'int', desc => 'Booking resource type'},
+            {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'},
+            {type => 'array', desc => 'Attribute values selected'},
+        ],
+        return => { desc => "A hash containing the new bresv and a list " .
+            "of new bravm"}
+    }
+);
+
+
+sub resource_list_by_attrs {
+    my $self = shift;
+    my $client = shift;
+    my $auth = shift; # Keep as argument, though not used just now.
+    my $filters = shift;
+
+    return undef unless ($filters->{type} || $filters->{attribute_values});
+
+    my $query = {
+        "select"   => {brsrc => ["id"]},
+        "from"     => {brsrc => {"brt" => {}}},
+        "where"    => {},
+        "distinct" => 1
+    };
+
+    $query->{where} = {"-and" => []};
+    if ($filters->{type}) {
+        push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}};
+    }
+
+    if ($filters->{pickup_lib}) {
+        push @{$query->{where}->{"-and"}},
+            {"-or" => [
+                {"owner" => $filters->{pickup_lib}},
+                {"+brt" => {"transferable" => "t"}}
+            ]};
+    }
+
+    if ($filters->{attribute_values}) {
+
+        $query->{from}->{brsrc}->{bram} = { field => 'resource' };
+
+        $filters->{attribute_values} = [$filters->{attribute_values}]
+            if (!ref($filters->{attribute_values}));
+
+        $query->{having}->{'+bram'}->{value}->{'@>'} = {
+            transform => 'array_accum',
+            value => '$_' . $$ . '${' .
+                join(',', @{$filters->{attribute_values}}) .
+                '}$_' . $$ . '$'
+        };
+    }
+
+    if ($filters->{available}) {
+        # If only one timestamp has been provided, make it into a range.
+        if (!ref($filters->{available})) {
+            $filters->{available} = [($filters->{available}) x 2];
+        }
+
+        push @{$query->{where}->{"-and"}}, {
+            "-or" => [
+                {"overbook" => "t"},
+                {"-not-exists" => {
+                    "select" => {"bresv" => ["id"]},
+                    "from" => "bresv",
+                    "where" => {"-and" => [
+                        json_query_ranges_overlap(
+                            $filters->{available}->[0],
+                            $filters->{available}->[1],
+                            "start_time",
+                            "end_time"
+                        ),
+                        {"cancel_time" => undef},
+                        {"current_resource" => {"=" => {"+brsrc" => "id"}}}
+                    ]},
+                }}
+            ]
+        };
+    }
+    if ($filters->{booked}) {
+        # If only one timestamp has been provided, make it into a range.
+        if (!ref($filters->{booked})) {
+            $filters->{booked} = [($filters->{booked}) x 2];
+        }
+
+        push @{$query->{where}->{"-and"}}, {
+            "-exists" => {
+                "select" => {"bresv" => ["id"]},
+                "from" => "bresv",
+                "where" => {"-and" => [
+                    json_query_ranges_overlap(
+                        $filters->{booked}->[0],
+                        $filters->{booked}->[1],
+                        "start_time",
+                        "end_time"
+                    ),
+                    {"cancel_time" => undef},
+                    {"current_resource" => { "=" => {"+brsrc" => "id"}}}
+                ]},
+            }
+        };
+        # I think that the "booked" case could be done with a JOIN instead of
+        # an EXISTS, but I'm leaving it this way for symmetry with the
+        # "available" case for now.  The available case cannot be done with a
+        # join.
+    }
+
+    my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
+    my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1);
+    $cstore->disconnect;
+
+    return @$rows ? [map { $_->{id} } @$rows] : [];
+}
+__PACKAGE__->register_method(
+    method   => "resource_list_by_attrs",
+    api_name => "open-ils.booking.resources.filtered_id_list",
+    argc     => 3,
+    signature=> {
+        params => [
+            {type => 'string', desc => 'Authentication token (unused for now,' .
+               ' but at least pass undef here)'},
+            {type => 'object', desc => 'Filter object: see notes for details'},
+            {type => 'bool', desc => 'Return whole objects instead of IDs?'}
+        ],
+        return => { desc => "An array of brsrc ids matching the requested filters." },
+    },
+    notes    => <<'NOTES'
+
+The filter object parameter can contain the following keys:
+ * type             => The id of a booking resource type (brt)
+ * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
+ * available        => Either:
+                        A timestamp during which the resources are not reserved.  If the resource is overbookable, this is ignored.
+                        A range of two timestamps which do not overlap any reservations for the resources.  If the resource is overbookable, this is ignored.
+ * booked           => Either:
+                        A timestamp during which the resources are reserved.
+                        A range of two timestamps which overlap a reservation of the resources.
+
+Note that at least one of 'type' or 'attribute_values' is required.
+
+NOTES
+);
+
+
+sub reservation_list_by_filters {
+    my $self = shift;
+    my $client = shift;
+    my $auth = shift;
+    my $filters = shift;
+    my $whole_obj = shift;
+
+    return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values});
+
+    my $e = new_editor(authtoken=>$auth);
+    return $e->event unless $e->checkauth;
+    return $e->event unless $e->allowed('VIEW_TRANSACTION');
+
+    my $query = {
+        'select'   => { bresv => [ 'id', 'start_time' ] },
+        'from'     => { bresv => {} },
+        'where'    => {},
+        'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }],
+        'distinct' => 1
+    };
+
+    if ($filters->{fields}) {
+        $query->{where} = $filters->{fields};
+    }
+
+
+    if ($filters->{user}) {
+        $query->{where}->{usr} = $filters->{user};
+    }
+    elsif ($filters->{user_barcode}) {  # just one of user and user_barcode
+        my $usr = $U->fetch_user_by_barcode($filters->{user_barcode});
+        return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"});
+        $query->{where}->{usr} = $usr->id;
+    }
+
+
+    if ($filters->{type}) {
+        $query->{where}->{target_resource_type} = $filters->{type};
+    }
+
+    if ($filters->{resource}) {
+        $query->{where}->{target_resource} = $filters->{resource};
+    }
+
+    if ($filters->{attribute_values}) {
+
+        $query->{from}->{bresv}->{bravm} = { field => 'reservation' };
+
+        $filters->{attribute_values} = [$filters->{attribute_values}]
+            if (!ref($filters->{attribute_values}));
+
+        $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = {
+            transform => 'array_accum',
+            value => '$_' . $$ . '${' .
+                join(',', @{$filters->{attribute_values}}) .
+                '}$_' . $$ . '$'
+        };
+    }
+
+    if ($filters->{search_start} || $filters->{search_end}) {
+        $query->{where}->{'-or'} = {};
+
+        $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
+                if ($filters->{search_start});
+
+        $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] }
+                if ($filters->{search_end});
+    }
+
+    my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
+    my $ids = [ map { $_->{id} } @{
+        $cstore->request(
+            'open-ils.cstore.json_query.atomic', $query
+        )->gather(1)
+    } ];
+    $cstore->disconnect;
+
+    if (not $whole_obj or @$ids < 1) {
+        $e->disconnect;
+        return $ids;
+    }
+
+    my $bresv_list = $e->search_booking_reservation([
+        {"id" => $ids},
+        {"flesh" => 1,
+            "flesh_fields" => {
+                "bresv" =>
+                    [qw/target_resource current_resource target_resource_type/]
+            }
+        }]
+    );
+    $e->disconnect;
+    return $bresv_list ? $bresv_list : [];
+}
+__PACKAGE__->register_method(
+    method   => "reservation_list_by_filters",
+    api_name => "open-ils.booking.reservations.filtered_id_list",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => 'string', desc => 'Authentication token'},
+            {type => 'object', desc => 'Filter object -- see notes for details'}
+        ],
+        return => { desc => "An array of bresv ids matching the requested filters." },
+    },
+    notes    => <<'NOTES'
+
+The filter object parameter can contain the following keys:
+ * user             => The id of a user that has requested a bookable item -- filters on bresv.usr
+ * barcode          => The barcode of a user that has requested a bookable item
+ * type             => The id of a booking resource type (brt) -- filters on bresv.target_resource_type
+ * resource         => The id of a booking resource (brsrc) -- filters on bresv.target_resource
+ * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav)
+ * search_start     => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp.
+ * search_end       => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp.
+ * fields           => An object containing any combination of bresv search filters in standard cstore/pcrud search format.
+
+Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required.  If both search_start and search_end are specified,
+then the result includes any reservations that overlap with that time range.  Any filter fields supplied in 'fields' are overridden
+by the top-level filters ('user', 'type', 'resource').
+
+NOTES
+);
+
+
+sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));}
+sub naive_start_of_day {strftime("%F", localtime($_[0] || time))." 00:00:00";}
+
+# Return a list of bresv or an ilsevent on failure.
+sub get_uncaptured_bresv_for_brsrc {
+    my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range
+
+    my $from_clause = {
+        "bresv" => {
+            "brsrc" => {"field" => "id", "fkey" => "current_resource"}
+        }
+    };
+
+    my $query = {
+        "select" => {
+            "bresv" => [
+                "current_resource",
+                {
+                    "column" => "start_time",
+                    "transform" => "min",
+                    "aggregate" => 1
+                }
+            ]
+        },
+        "from" => $from_clause,
+        "where" => {
+            "-and" => [
+                {"current_resource" => {"!=" => undef}},
+                {"capture_time" => undef},
+                {"cancel_time" => undef},
+                {"return_time" => undef},
+                {"pickup_time" => undef}
+            ]
+        }
+    };
+    if ($o->{"owning_lib"}) {
+        push @{$query->{"where"}->{"-and"}},
+            {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
+    }
+    if ($o->{"range"}) {
+        push @{$query->{"where"}->{"-and"}},
+            json_query_ranges_overlap(
+                $o->{"range"}->[0], $o->{"range"}->[1],
+                "start_time", "end_time"
+            );
+    }
+    if ($o->{"barcode"}) {
+        push @{$query->{"where"}->{"-and"}},
+            {"+brsrc" => {"barcode" => $o->{"barcode"}}};
+    }
+
+    my $rows = $e->json_query($query);
+    my $current_resource_bresv_map = {};
+    if (@$rows) {
+        my $id_query = {
+            "select" => {"bresv" => ["id"]},
+            "from" => $from_clause,
+            "where" => {
+                "-and" => [
+                    {"current_resource" => "PLACEHOLDER"},
+                    {"start_time" => "PLACEHOLDER"},
+                ]
+            }
+        };
+        if ($o->{"owning_lib"}) {
+            push @{$id_query->{"where"}->{"-and"}},
+                {"+brsrc" => {"owner" => $o->{"owning_lib"}}};
+        }
+
+        foreach (@$rows) {
+            $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} =
+                $_->{"current_resource"};
+            $id_query->{"where"}->{"-and"}->[1]->{"start_time"} =
+                $_->{"start_time"};
+
+            my $results = $e->json_query($id_query);
+            if ($results && @$results) {
+                $current_resource_bresv_map->{$_->{"current_resource"}} =
+                    [map { $_->{"id"} } @$results];
+            }
+        }
+    }
+    return $current_resource_bresv_map;
+}
+
+sub get_pull_list {
+    my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST");
+    return $e->die_event unless (
+        ref($range) eq "ARRAY" or
+        ($interval_secs = int($interval_secs)) > 0
+    );
+
+    $owning_lib = $e->requestor->ws_ou if not $owning_lib;
+    $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ]
+        if not $range;
+
+    my $uncaptured = get_uncaptured_bresv_for_brsrc(
+        $e, {"range" => $range, "owning_lib" => $owning_lib}
+    );
+
+    if (keys(%$uncaptured)) {
+        my @all_bresv_ids = map { @{$_} } values %$uncaptured;
+        my %bresv_lookup = (
+            map { $_->id => $_ } @{
+                $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, {
+                    flesh => 1,
+                    flesh_fields => { bresv => [
+                        "usr", "target_resource_type", "current_resource"
+                    ]}
+                }])
+            }
+        );
+        $e->disconnect;
+        return [ map {
+            my $key = $_;
+            my $one = $bresv_lookup{$uncaptured->{$key}->[0]};
+            my $result = {
+                "current_resource" => $one->current_resource,
+                "target_resource_type" => $one->target_resource_type,
+                "reservations" => [
+                    map { $bresv_lookup{$_} } @{$uncaptured->{$key}}
+                ]
+            };
+            foreach (@{$result->{"reservations"}}) {    # deflesh
+                $_->current_resource($_->current_resource->id);
+                $_->target_resource_type($_->target_resource_type->id);
+            }
+            $result;
+        } keys %$uncaptured ];
+    } else {
+        $e->disconnect;
+        return [];
+    }
+}
+__PACKAGE__->register_method(
+    method   => "get_pull_list",
+    api_name => "open-ils.booking.reservations.get_pull_list",
+    argc     => 4,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "array", desc =>
+                "range: Date/time range for reservations (opt)"},
+            {type => "int", desc =>
+                "interval: Seconds from now (instead of range)"},
+            {type => "number", desc => "(Optional) Owning library"}
+        ],
+        return => { desc => "An array of hashes, each containing key/value " .
+            "pairs describing resource, resource type, and a list of " .
+            "reservations that claim the given resource." }
+    }
+);
+
+
+sub get_copy_fleshed_just_right {
+    my ($self, $client, $auth, $barcode) = @_;
+
+    return undef if not defined $barcode;
+    return {} if ref($barcode) eq "ARRAY" and not @$barcode;
+
+    my $e = new_editor(authtoken => $auth);
+    my $results = $e->search_asset_copy([
+        {"barcode" => $barcode},
+        {
+            "flesh" => 1,
+            "flesh_fields" => {"acp" => [qw/call_number location/]}
+        }
+    ]);
+
+    if (ref($results) eq "ARRAY") {
+        $e->disconnect;
+        return $results->[0] unless ref $barcode;
+        return +{ map { $_->barcode => $_ } @$results };
+    } else {
+        return $e->die_event;
+    }
+}
+__PACKAGE__->register_method(
+    method   => "get_copy_fleshed_just_right",
+    api_name => "open-ils.booking.asset.get_copy_fleshed_just_right",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "mixed", desc => "One barcode or an array of them"},
+        ],
+        return => { desc =>
+            "A copy, or a hash of copies keyed by barcode if an array of " .
+            "barcodes was given"
+        }
+    }
+);
+
+
+sub best_bresv_candidate {
+    my ($e, $id_list) = @_;
+
+    # This will almost always be the case.
+    return $id_list->[0] if @$id_list == 1;
+
+    my @here = ();
+    my $this_ou = $e->requestor->ws_ou;
+    my $results = $e->json_query({
+        "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]},
+        "from" => {
+            "bresv" => {
+                "brsrc" => {"field" => "id", "fkey" => "current_resource"}
+            }
+        },
+        "where" => {
+            {"+bresv" => {"id" => $id_list}}
+        }
+    });
+
+    foreach (@$results) {
+        push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou;
+    }
+
+    if (@here > 0) {
+        return pop @here if @here == 1;
+        return (sort @here)[0];
+    } else {
+        return (sort @$id_list)[0];
+    }
+}
+
+
+sub capture_resource_for_reservation {
+    my ($self, $client, $auth, $barcode) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed("CAPTURE_RESERVATION");
+
+    my $uncaptured = get_uncaptured_bresv_for_brsrc(
+        $e, {"barcode" => $barcode}
+    );
+    $e->disconnect;
+
+    if (keys %$uncaptured) {
+        # Note this will only capture one reservation at a time, even in
+        # cases with overbooking (multiple "soonest" bresv's on a resource).
+        my $key = (sort(keys %$uncaptured))[0];
+        return capture_reservation(
+            $self, $client, $auth, best_bresv_candidate($e, $uncaptured->{$key})
+        );
+    } else {
+        return new OpenILS::Event(
+            "RESERVATION_NOT_FOUND",
+            desc => "No capturable reservation found pertaining " .
+                "to a resource with barcode $barcode",
+            payload => {fail_cause => 'no-reservation', captured => 0}
+        );
+    }
+}
+__PACKAGE__->register_method(
+    method   => "capture_resource_for_reservation",
+    api_name => "open-ils.booking.resources.capture_for_reservation",
+    argc     => 3,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "string", desc => "Barcode of booked & targeted resource"},
+            {type => "int", desc => "Pickup library (default to client ws_ou)"},
+        ],
+        return => { desc => "An OpenILS event describing the capture outcome" }
+    }
+);
+
+
+sub capture_reservation {
+    my ($self, $client, $auth, $res_id) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    return $e->event unless $e->allowed('CAPTURE_RESERVATION');
+    my $here = $e->requestor->ws_ou;
+
+    my $reservation = $e->retrieve_booking_reservation([
+        $res_id, {
+            flesh => 2,
+            flesh_fields => {"bresv" => ["usr"], "au" => ["card"]}
+        }
+    ]);
+    return OpenILS::Event->new('RESERVATION_NOT_FOUND') unless $reservation;
+
+    return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'no-resource' })
+        if (!$reservation->current_resource); # no resource
+
+    return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'cancelled' })
+        if ($reservation->cancel_time); # canceled
+
+    my $resource = $e->retrieve_booking_resource( $reservation->current_resource );
+    my $type = $e->retrieve_booking_resource_type( $resource->type );
+
+    $reservation->capture_staff( $e->requestor->id );
+    $reservation->capture_time( 'now' );
+
+    my $reservation_id = undef;
+    return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation_id = $e->data );
+
+    $reservation->id($reservation_id);
+
+    my $ret = { captured => 1, reservation => $reservation };
+
+    if ($here <> $reservation->pickup_lib) {
+        return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'not-transferable' })
+            if (!$U->is_true($type->transferable)); # non-transferable resource
+
+        # need to transit the item ... is it already in transit?
+        my $transit = $e->search_action_reservation_transit_copy( { reservation => $res_id, dest_recv_time => undef } )->[0];
+
+        if (!$transit) { # not yet in transit
+            $transit = new Fieldmapper::action::reservation_transit_copy;
+
+            $transit->reservation($reservation->id);
+            $transit->target_copy($resource->id);
+            $transit->copy_status(15);
+            $transit->source_send_time('now');
+            $transit->source($here);
+            $transit->dest($reservation->pickup_lib);
+
+            $e->create_action_reservation_transit_copy( $transit );
+
+            if ($U->is_true($type->catalog_item)) {
+                my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
+
+                if ($copy) {
+                    return new OpenILS::Event(
+                        "OPEN_CIRCULATION_EXISTS",
+                        payload => { captured => 0, copy => $copy }
+                    ) if $copy->status == 1;
+                    $copy->status(6);
+                    $e->update_asset_copy( $copy );
+                    $$ret{catalog_item} = $copy; # $e->data is just id (int)
+                }
+            }
+        }
+
+        $$ret{transit} = $transit;
+    } elsif ($U->is_true($type->catalog_item)) {
+        my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0];
+
+        if ($copy) {
+            return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => { captured => 0, copy => $copy }) if ($copy->status == 1);
+            $copy->status(15);
+            $e->update_asset_copy( $copy );
+            $$ret{catalog_item} = $copy; # $e->data is just id (int)
+        }
+    }
+
+    $e->commit;
+
+    return OpenILS::Event->new('SUCCESS', payload => $ret);
+}
+__PACKAGE__->register_method(
+    method   => "capture_reservation",
+    api_name => "open-ils.booking.reservations.capture",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => 'string', desc => 'Authentication token'},
+            {type => 'mixed', desc =>
+                'Reservation ID (number) or array of resource barcodes'}
+        ],
+        return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." },
+    }
+);
+
+
+sub cancel_reservation {
+    my ($self, $client, $auth, $id_list) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    # Should the following permission really be checked as relates to each
+    # individual reservation's request_lib?  Hrmm...
+    return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
+
+    my $bresv_list = $e->search_booking_reservation([
+        {"id" => $id_list},
+        {"flesh" => 1, "flesh_fields" => {"bresv" => [
+            "current_resource", "target_resource_type"
+        ]}}
+    ]);
+    return $e->die_event if not $bresv_list;
+
+    my $circ = OpenSRF::AppSession->connect("open-ils.circ") or
+        return $e->die_event;
+    my @results = ();
+    foreach my $bresv (@$bresv_list) {
+        if (
+            $bresv->target_resource_type->catalog_item == "t" &&
+            $bresv->current_resource
+        ) {
+            $logger->info("result of no-op checkin (upon cxl bresv) is " .
+                $circ->request(
+                    "open-ils.circ.checkin", $auth,
+                    {"barcode" => $bresv->current_resource->barcode,
+                        "noop" => 1}
+                )->gather(1)->{"textcode"});
+        }
+        $bresv->cancel_time("now");
+        $e->update_booking_reservation($bresv) or do {
+            $circ->disconnect;
+            return $e->die_event;
+        };
+
+        push @results, $bresv->id;
+    }
+
+    $e->commit;
+    $circ->disconnect;
+
+    return \@results;
+}
+__PACKAGE__->register_method(
+    method   => "cancel_reservation",
+    api_name => "open-ils.booking.reservations.cancel",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "array", desc => "List of reservation IDs"}
+        ],
+        return => { desc => "A list of canceled reservation IDs" },
+    }
+);
+
+
+sub get_captured_reservations {
+    my ($self, $client, $auth, $barcode, $which) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed("VIEW_USER");
+    return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
+
+    # fetch the patron for our uses in any case...
+    my $patron = $U->fetch_user_by_barcode($barcode);
+    return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"};
+
+    my $bresv_flesh = {
+        "flesh" => 1,
+        "flesh_fields" => {"bresv" => [
+            qw/target_resource_type current_resource/
+        ]}
+    };
+
+    my $dispatch = {
+        "patron" => sub {
+            return $patron;
+        },
+        "ready" => sub {
+            return $e->search_booking_reservation([
+                {
+                    "usr" => $patron->id,
+                    "capture_time" => {"!=" => undef},
+                    "pickup_time" => undef,
+                    "start_time" => {">=" => naive_start_of_day()},
+                    "cancel_time" => undef
+                },
+                $bresv_flesh
+            ]) or $e->die_event;
+        },
+        "out" => sub {
+            return $e->search_booking_reservation([
+                {
+                    "usr" => $patron->id,
+                    "pickup_time" => {"!=" => undef},
+                    "return_time" => undef,
+                    "cancel_time" => undef
+                },
+                $bresv_flesh
+            ]) or $e->die_event;
+        },
+        "in" => sub {
+            return $e->search_booking_reservation([
+                {
+                    "usr" => $patron->id,
+                    "return_time" => {">=" => naive_start_of_day()},
+                    "cancel_time" => undef
+                },
+                $bresv_flesh
+            ]) or $e->die_event;
+        }
+    };
+
+    my $result = {};
+    foreach (@$which) {
+        my $f = $dispatch->{$_};
+        if ($f) {
+            my $r = &{$f}();
+            return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"});
+            $result->{$_} = $r;
+        }
+    }
+
+    return $result;
+}
+__PACKAGE__->register_method(
+    method   => "get_captured_reservations",
+    api_name => "open-ils.booking.reservations.get_captured",
+    argc     => 3,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "string", desc => "Patron barcode"},
+            {type => "array", desc => "Parts wanted (patron, ready, out, in?)"}
+        ],
+        return => { desc => "A hash of parts." } # XXX describe more fully
+    }
+);
+
+
+sub get_bresv_by_returnable_resource_barcode {
+    my ($self, $client, $auth, $barcode) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+    return $e->die_event unless $e->allowed("VIEW_USER");
+    return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION");
+
+    my $rows = $e->json_query({
+        "select" => {"bresv" => ["id"]},
+        "from" => {
+            "bresv" => {
+                "brsrc" => {"field" => "id", "fkey" => "current_resource"}
+            }
+        },
+        "where" => {
+            "+brsrc" => {"barcode" => $barcode},
+            "-and" => {
+                "pickup_time" => {"!=" => undef},
+                "cancel_time" => undef,
+                "return_time" => undef
+            }
+        }
+    }) or return $e->die_event;
+
+    if (@$rows < 1) {
+        return $rows;
+    } else {
+        # More than one result might be possible, but we don't want to return
+        # more than one at this time.
+        my $id = $rows->[0]->{"id"};
+        return $e->retrieve_booking_reservation([
+            $id, {
+                "flesh" => 2,
+                "flesh_fields" => {
+                    "bresv" => [qw/usr target_resource_type current_resource/],
+                    "au" => ["card"]
+                }
+            }
+        ]) or $e->die_event;
+    }
+}
+
+__PACKAGE__->register_method(
+    method   => "get_bresv_by_returnable_resource_barcode",
+    api_name => "open-ils.booking.reservations.by_returnable_resource_barcode",
+    argc     => 2,
+    signature=> {
+        params => [
+            {type => "string", desc => "Authentication token"},
+            {type => "string", desc => "Resource barcode"},
+        ],
+        return => { desc => "A fleshed bresv or an ilsevent on error" }
+    }
+);
+
+
+1;
index e9867b1..8aae114 100644 (file)
@@ -1105,6 +1105,144 @@ sub test_batch_circ_events {
 }
 
 
+# XXX
+# XXX !!! HERE through line 1245 !!!
+# XXX Backported from trunk aprox 15310, may not be supported or required by 1.6
+# XXX
+
+__PACKAGE__->register_method(
+       method  => "user_payments_list",
+       api_name        => "open-ils.circ.user_payments.filtered.batch",
+    stream => 1,
+       signature => {
+        desc => q/Returns a fleshed, date-limited set of all payments a user
+                has made.  By default, ordered by payment date.  Optionally
+                ordered by other columns in the top-level "mp" object/,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'User ID', type => 'number'},
+            {desc => 'Order by column(s), optional.  Array of "mp" class columns', type => 'array'}
+        ],
+        return => {desc => q/List of "mp" objects, fleshed with the billable transaction 
+            and the related fully-realized payment object (e.g money.cash_payment)/}
+    }
+);
+
+sub user_payments_list {
+    my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+
+    my $user = $e->retrieve_actor_user($user_id) or return $e->event;
+    return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou);
+
+    $order_by ||= ['payment_ts'];
+
+    # all payments by user, between start_date and end_date
+    my $payments = $e->json_query({
+        select => {mp => ['id']}, 
+        from => {
+            mp => {
+                mbt => {
+                    fkey => 'xact', field => 'id'}
+            }
+        }, 
+        where => {
+            '+mbt' => {usr => $user_id}, 
+            '+mp' => {payment_ts => {between => [$start_date, $end_date]}}
+        },
+        order_by => {mp => $order_by}
+    });
+
+    for my $payment_id (@$payments) {
+        my $payment = $e->retrieve_money_payment([
+            $payment_id->{id}, 
+            {   
+                flesh => 2,
+                flesh_fields => {
+                    mp => [
+                        'xact',
+                        'cash_payment',
+                        'credit_card_payment',
+                        'credit_payment',
+                        'check_payment',
+                        'work_payment',
+                        'forgive_payment',
+                        'goods_payment'
+                    ],
+                    mbt => [
+                        'circulation', 
+                        'grocery',
+                        'reservation'
+                    ]
+                }
+            }
+        ]);
+        $conn->respond($payment);
+    }
+
+    return undef;
+}
+
+
+__PACKAGE__->register_method(
+       method  => "retrieve_circ_chain",
+       api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ",
+    stream => 1,
+       signature => {
+        desc => q/Given a circulation, this returns all circulation objects
+                that are part of the same chain of renewals./,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Circ ID', type => 'number'},
+        ],
+        return => {desc => q/List of circ objects, orderd by oldest circ first/}
+    }
+);
+
+__PACKAGE__->register_method(
+       method  => "retrieve_circ_chain",
+       api_name        => "open-ils.circ.renewal_chain.retrieve_by_circ.summary",
+       signature => {
+        desc => q/Given a circulation, this returns all circulation objects
+                that are part of the same chain of renewals./,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Circ ID', type => 'number'},
+        ],
+        return => {desc => q/List of circ objects, orderd by oldest circ first/}
+    }
+);
+
+sub retrieve_circ_chain {
+    my($self, $conn, $auth, $circ_id) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+       return $e->event unless $e->allowed('VIEW_CIRCULATIONS');
+
+    if($self->api_name =~ /summary/) {
+        my $sum = $e->json_query({from => ['action.summarize_circ_chain', $circ_id]})->[0];
+        return undef unless $sum;
+        my $obj = Fieldmapper::action::circ_chain_summary->new;
+        $obj->$_($sum->{$_}) for keys %$sum;
+        return $obj;
+
+    } else {
+
+        my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]});
+
+        for my $circ_info (@$chain) {
+            my $circ = Fieldmapper::action::circulation->new;
+            $circ->$_($circ_info->{$_}) for keys %$circ_info;
+            $conn->respond($circ);
+        }
+    }
+
+    return undef;
+}
+
 
 
 # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}}
index 6b56bbb..1306145 100644 (file)
@@ -2,10 +2,12 @@ package OpenILS::Application::Circ::Circulate;
 use strict; use warnings;
 use base 'OpenILS::Application';
 use OpenSRF::EX qw(:try);
+use OpenSRF::AppSession;
 use OpenSRF::Utils::SettingsClient;
 use OpenSRF::Utils::Logger qw(:logger);
 use OpenILS::Const qw/:const/;
 use OpenILS::Application::AppUtils;
+use DateTime;
 my $U = "OpenILS::Application::AppUtils";
 
 my %scripts;
@@ -148,6 +150,13 @@ __PACKAGE__->register_method(
 
 __PACKAGE__->register_method(
     method  => "run_method",
+    api_name    => "open-ils.circ.reservation.pickup");
+__PACKAGE__->register_method(
+    method  => "run_method",
+    api_name    => "open-ils.circ.reservation.return");
+
+__PACKAGE__->register_method(
+    method  => "run_method",
     api_name    => "open-ils.circ.checkout.inspect",
     desc => q/
         Returns the circ matrix test result and, on success, the rule set and matrix test object
@@ -167,9 +176,98 @@ sub run_method {
     return circ_events($circulator) if $circulator->bail_out;
 
     # --------------------------------------------------------------------------
+    # First, check for a booking transit, as the barcode may not be a copy
+    # barcode, but a resource barcode, and nothing else in here will work
+    # --------------------------------------------------------------------------
+
+    if ((my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode?
+        my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode?
+        if (@$resources) { # yes!
+
+            my $res_id_list = [ map { $_->id } @$resources ];
+            my $transit = $circulator->editor->search_action_reservation_transit_copy(
+                [
+                    { target_copy => $res_id_list, dest => $circulator->circ_lib },
+                    { order_by => { artc => 'source_send_time' }, limit => 1 }
+                ]
+            )->[0]; # Any transit for this barcode?
+
+            if ($transit) { # yes! unwrap it.
+
+                my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation );
+                my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
+
+                if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here?
+                    if (my $copy = $circulator->editor->search_asset_copy({ barcode => $bc, deleted => 'f' })->[0]) { # got a copy
+                        $copy->status( $transit->copy_status );
+                        $copy->editor($circulator->editor->requestor->id);
+                        $copy->edit_date('now');
+                        $circulator->editor->update_asset_copy( $copy );
+                    }
+                }
+
+                $transit->dest_recv_time('now');
+                $circulator->editor->update_action_reservation_transit_copy( $transit );
+
+                $circulator->editor->commit;
+
+                #XXX need to return here, with info about the resource/copy and the "put it on the booking shelf" message
+
+            } else { # no transit, look for an upcoming reservation to capture for
+
+                my $reservation = $circulator->editor->search_booking_reservation(
+                    [
+                        { current_resource => $res_id_list,
+                          pickup_lib => $circulator->circ_lib,
+                          cancel_time => undef,
+                          capture_time => undef
+                        },
+                        { order_by => { bresv => 'start_time' }, limit => 1 }
+                    ]
+                )->[0];
+
+                if ($reservation) { # we have a reservation for which we could capture this resource.  wheee!
+                    my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type );
+                    my $elbow_room = $res_type->elbow_room ||
+                        $U->ou_ancestor_setting_value( $circulator->circ_lib, 'circ.booking_reservation.default_elbow_room', $circulator->editor );
+                
+                    if ($elbow_room) {
+                        $reservation = $circulator->editor->search_booking_reservation(
+                            [
+                                { id => $reservation->id, start_time => { '<=' => DateTime->now->add( seconds => interval_to_seconds($elbow_room) )->strftime('%FT%T%z') } },
+                                { order_by => { bresv => 'start_time' }, limit => 1 }
+                            ]
+                        )->[0];
+                    }
+
+                    if ($reservation) { # no elbow room specified, or we still have a reservation within the elbow_room time
+                        my $b_ses = OpenSRF::AppSession->create('open-ils.booking');
+                        my $result = $b_ses->request(
+                            'open-ils.booking.reservations.capture',
+                            $auth => $reservation->id
+                        )->gather(1);
+
+                        if (ref($result) && $result->{ilsevent} == 0) { # captured!
+                            #XXX what to return here???
+                            return $result; # the booking capture success
+                        } else {
+                            #XXX how to fail???  Probably, just move on.
+                        }
+                    }
+                }
+            }
+        }
+    }
+            
+    
+
+    # --------------------------------------------------------------------------
     # Go ahead and load the script runner to make sure we have all 
     # of the objects we need
     # --------------------------------------------------------------------------
+    $circulator->is_res_checkin($circulator->is_checkin(1)) if $api =~ /reservation.return/;
+    $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/;
+
     $circulator->is_renewal(1) if $api =~ /renew/;
     $circulator->is_checkin(1) if $api =~ /checkin/;
 
@@ -180,7 +278,7 @@ sub run_method {
         $circulator->circ_permit_copy($scripts{circ_permit_copy});      
         $circulator->circ_duration($scripts{circ_duration});             
         $circulator->circ_permit_renew($scripts{circ_permit_renew});
-    } else {
+    } elsif (not $circulator->is_res_checkin) { # mk_env cannot work w/ reservation.return
         $circulator->mk_env();
     }
     return circ_events($circulator) if $circulator->bail_out;
@@ -203,6 +301,9 @@ sub run_method {
             $circulator->do_checkout();
         }
 
+    } elsif( $circulator->is_res_checkout ) {
+        $circulator->do_reservation_pickup();
+
     } elsif( $api =~ /inspect/ ) {
         my $data = $circulator->do_inspect();
         $circulator->editor->rollback;
@@ -212,6 +313,9 @@ sub run_method {
         $circulator->is_checkout(1);
         $circulator->do_checkout();
 
+    } elsif( $circulator->is_res_checkin ) {
+        $circulator->do_reservation_return();
+        $circulator->do_checkin() if ($circulator->copy());
     } elsif( $api =~ /checkin/ ) {
         $circulator->do_checkin();
 
@@ -335,6 +439,7 @@ my @AUTOLOAD_FIELDS = qw/
     notify_hold
     remote_hold
     backdate
+    reservation
     copy
     copy_id
     copy_barcode
@@ -346,10 +451,12 @@ my @AUTOLOAD_FIELDS = qw/
     title
     is_renewal
     is_checkout
+    is_res_checkout
     is_noncat
     is_precat
     request_precat
     is_checkin
+    is_res_checkin
     noncat_type
     editor
     events
@@ -1163,7 +1270,10 @@ sub do_checkout {
     $self->build_checkout_circ_object();
     return if $self->bail_out;
 
-    $self->apply_modified_due_date();
+    my $modify_to_start = $self->booking_adjusted_due_date();
+    return if $self->bail_out;
+
+    $self->apply_modified_due_date($modify_to_start);
     return if $self->bail_out;
 
     return $self->bail_on_events($self->editor->event)
@@ -1258,6 +1368,28 @@ sub update_copy {
     $copy->circ_lib($circ_lib) if $circ_lib;
 }
 
+sub update_reservation {
+    my $self = shift;
+    my $reservation = $self->reservation;
+
+    my $usr = $reservation->usr;
+    my $target_rt = $reservation->target_resource_type;
+    my $target_r = $reservation->target_resource;
+    my $current_r = $reservation->current_resource;
+
+    $reservation->usr($usr->id) if ref $usr;
+    $reservation->target_resource_type($target_rt->id) if ref $target_rt;
+    $reservation->target_resource($target_r->id) if ref $target_r;
+    $reservation->current_resource($current_r->id) if ref $current_r;
+
+    return $self->bail_on_events($self->editor->event)
+        unless $self->editor->update_booking_reservation($self->reservation);
+
+    my $evt;
+    ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id);
+    $self->reservation($reservation);
+}
+
 
 sub bail_on_events {
     my( $self, @evts ) = @_;
@@ -1340,6 +1472,7 @@ sub handle_checkout_holds {
 
 sub run_checkout_scripts {
     my $self = shift;
+    my $nobail = shift;
 
     my $evt;
     my $runner = $self->script_runner;
@@ -1374,13 +1507,13 @@ sub run_checkout_scripts {
 
         unless($duration) {
             ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name);
-            return $self->bail_on_events($evt) if $evt;
+            return $self->bail_on_events($evt) if ($evt && !$nobail);
         
             ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name);
-            return $self->bail_on_events($evt) if $evt;
+            return $self->bail_on_events($evt) if ($evt && !$nobail);
         
             ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name);
-            return $self->bail_on_events($evt) if $evt;
+            return $self->bail_on_events($evt) if ($evt && !$nobail);
         }
 
     } else {
@@ -1462,9 +1595,151 @@ sub build_checkout_circ_object {
     $self->circ($circ);
 }
 
+sub do_reservation_pickup {
+    my $self = shift;
+
+    $self->log_me("do_reservation_pickup()");
+
+    $self->reservation->pickup_time('now');
+
+    if (
+        $self->reservation->current_resource &&
+        $self->reservation->current_resource->catalog_item
+    ) {
+        $self->copy( $self->reservation->current_resource->catalog_item );
+        $self->patron( $self->reservation->usr );
+        $self->run_checkout_scripts(1);
+
+        my $duration   = $self->duration_rule;
+        my $max        = $self->max_fine_rule;
+        my $recurring  = $self->recurring_fines_rule;
+
+        if ($duration && $max && $recurring) {
+            my $policy = $self->get_circ_policy($duration, $recurring, $max);
+
+            my $dname = $duration->name;
+            my $mname = $max->name;
+            my $rname = $recurring->name;
+
+            $logger->debug("circulator: building reservation ".
+                "with duration=$dname, maxfine=$mname, recurring=$rname");
+
+            $self->reservation->fine_amount($policy->{recurring_fine});
+            $self->reservation->max_fine($policy->{max_fine});
+            $self->reservation->fine_interval($recurring->recurrence_interval);
+        }
+
+        $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT);
+        $self->update_copy();
+
+    } else {
+        $self->reservation->fine_amount($self->reservation->fine_amount);
+        $self->reservation->max_fine($self->reservation->max_fine);
+        $self->reservation->fine_interval($self->reservation->fine_interval);
+    }
+
+    $self->update_reservation();
+}
+
+sub do_reservation_return {
+    my $self = shift;
+    my $request = shift;
+
+    $self->log_me("do_reservation_return()");
+
+    my ($reservation, $evt) = $U->fetch_booking_reservation($self->reservation);
+    return $self->bail_on_events($evt) if $evt;
+
+    $self->reservation( $reservation );
+    $self->generate_fines(1);
+    $self->reservation->return_time('now');
+    $self->update_reservation();
+
+    if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) {
+        $self->copy( $self->reservation->current_resource->catalog_item );
+    }
+}
+
+sub booking_adjusted_due_date {
+    my $self = shift;
+    my $circ = $self->circ;
+    my $copy = $self->copy;
+
+
+    my $changed;
+
+    if( $self->due_date ) {
+
+        return $self->bail_on_events($self->editor->event)
+            unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
+
+       $circ->due_date(clense_ISO8601($self->due_date));
+
+    } else {
+
+        return unless $copy and $circ->due_date;
+    }
+
+    my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } );
+    if (@$booking_items) {
+        my $booking_item = $booking_items->[0];
+        my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type );
+
+        my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor );
+        my $shorten_circ_setting = $resource_type->elbow_room ||
+            $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) ||
+            '0 seconds';
+
+        my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' );
+        my $bookings = $booking_ses->request(
+            'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken,
+            { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date }
+        )->gather(1);
+        $booking_ses->disconnect;
+        
+        my $dt_parser = DateTime::Format::ISO8601->new;
+        my $due_date = $dt_parser->parse_datetime( clense_ISO8601($circ->due_date) );
+
+        for my $bid (@$bookings) {
+
+            my $booking = $self->editor->retrieve_booking_reservation( $bid );
+
+            my $booking_start = $dt_parser->parse_datetime( clense_ISO8601($booking->start_time) );
+            my $booking_end = $dt_parser->parse_datetime( clense_ISO8601($booking->end_time) );
+
+            return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') )
+                if ($booking_start < DateTime->now);
+
+
+            if ($U->is_true($stop_circ_setting)) {
+                $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); 
+            } else {
+                $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) );
+                $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); 
+            }
+            
+            # We set the circ duration here only to affect the logic that will
+            # later (in a DB trigger) mangle the time part of the due date to
+            # 11:59pm. Having any circ duration that is not a whole number of
+            # days is enough to prevent the "correction."
+            my $new_circ_duration = $due_date->epoch - time;
+            $new_circ_duration++ if $new_circ_duration % 86400 == 0;
+            $circ->duration("$new_circ_duration seconds");
+
+            $circ->due_date(clense_ISO8601($due_date->strftime('%FT%T%z')));
+            $changed = 1;
+        }
+
+        return $self->bail_on_events($self->editor->event)
+            unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib);
+    }
+
+    return $changed;
+}
 
 sub apply_modified_due_date {
     my $self = shift;
+    my $shift_earlier = shift;
     my $circ = $self->circ;
     my $copy = $self->copy;
 
@@ -1499,7 +1774,11 @@ sub apply_modified_due_date {
 
             # XXX make the behavior more dynamic
             # for now, we just push the due date to after the close date
-            $circ->due_date($dateinfo->{end});
+            if ($shift_earlier) {
+                $circ->due_date($dateinfo->{start});
+            } else {
+                $circ->due_date($dateinfo->{end});
+            }
       }
    }
 }
@@ -1721,7 +2000,7 @@ sub do_checkin {
    # this copy can fulfill a hold or needs to be routed to a different location
    # ------------------------------------------------------------------------------
 
-    unless($self->noop) { # no-op checkins to not capture holds or put items into transit
+    if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
 
         my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture());
         return if $self->bail_out;
@@ -2110,6 +2389,53 @@ sub process_received_transit {
 }
 
 
+# ------------------------------------------------------------------
+# Sets the shelf_time and shelf_expire_time for a newly shelved hold
+# ------------------------------------------------------------------
+sub put_hold_on_shelf {
+    my($self, $hold) = @_;
+
+    $hold->shelf_time('now');
+
+    my $shelf_expire = $U->ou_ancestor_setting_value(
+        $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor);
+
+    if($shelf_expire) {
+        my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire);
+        my $expire_time = DateTime->now->add(seconds => $seconds);
+        $hold->shelf_expire_time($expire_time->strftime('%FT%T%z'));
+    }
+
+    return undef;
+}
+
+
+
+sub generate_fines {
+   my $self = shift;
+   my $reservation = shift;
+   my $evt;
+   my $obt;
+
+   my $id = $reservation ? $self->reservation->id : $self->circ->id;
+
+   my $st = OpenSRF::AppSession->connect('open-ils.storage');
+
+   $st->request(
+      'open-ils.storage.action.circulation.overdue.generate_fines',
+      undef,
+      $id
+   )->wait_complete;
+
+   $st->disconnect;
+
+   # refresh the circ in case the fine generator set the stop_fines field
+   $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation;
+   $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation;
+
+   return undef;
+}
+
 sub checkin_handle_circ {
    my $self = shift;
    my $circ = $self->circ;
@@ -2292,6 +2618,7 @@ sub check_checkin_copy_status {
             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
             $status == OILS_COPY_STATUS_IN_TRANSIT  ||
             $status == OILS_COPY_STATUS_CATALOGING  ||
+            $status == OILS_COPY_STATUS_ON_RESV_SHELF  ||
             $status == OILS_COPY_STATUS_RESHELVING );
 
    return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy )
index ad8da67..e971c47 100644 (file)
@@ -270,8 +270,22 @@ sub create_grocery_bill {
 
 
 __PACKAGE__->register_method(
-       method => 'fetch_grocery',
-       api_name => 'open-ils.circ.money.grocery.retrieve'
+    method => 'fetch_reservation',
+    api_name => 'open-ils.circ.booking.reservation.retrieve'
+);
+sub fetch_reservation {
+    my( $self, $conn, $auth, $id ) = @_;
+    my $e = new_editor(authtoken=>$auth);
+    return $e->event unless $e->checkauth;
+    return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission
+    my $g = $e->retrieve_booking_reservation($id)
+        or return $e->event;
+    return $g;
+}
+
+__PACKAGE__->register_method(
+    method => 'fetch_grocery',
+    api_name => 'open-ils.circ.money.grocery.retrieve'
 );
 
 sub fetch_grocery {
index 050a9d1..8b72ae9 100644 (file)
@@ -621,7 +621,9 @@ sub transaction_details {
                        circulations    => 
                                fetch_circ_xacts($e, $uid, $org, $start_date, $end_date),
                        grocery                 => 
-                               fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date)
+                               fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date),
+                       reservations    => 
+                               fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date)
                };
 
                # for each transaction, flesh the workstatoin on any attached payment
@@ -629,6 +631,7 @@ sub transaction_details {
                # not just a generic payment object
                for my $xact ( 
                        @{$blob->{transactions}->{circulations}}, 
+                       @{$blob->{transactions}->{reservations}}, 
                        @{$blob->{transactions}->{grocery}} ) {
 
                        my $ps;
@@ -778,6 +781,53 @@ sub fetch_grocery_xacts {
        return \@data;
 }
 
+sub fetch_reservation_xacts {
+       my $e                           = shift;
+       my $uid                 = shift;
+       my $org                 = shift;
+       my $start_date = shift;
+       my $end_date    = shift;
+
+       my @xacts;
+       $U->walk_org_tree( $org, 
+               sub {
+                       my $n = shift;
+                       $logger->debug("collect: searching for open grocery xacts at " . $n->shortname);
+                       push( @xacts, 
+                               @{
+                                       $e->search_booking_reservation(
+                                               {
+                                                       usr                                     => $uid, 
+                                                       pickup_lib              => $n->id,
+                                               }, 
+                                               {idlist => 1}
+                                       )
+                               }
+                       );
+               }
+       );
+
+       my @data;
+       my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date);
+
+       for my $id (@$active_ids) {
+               push( @data, 
+                       $e->retrieve_booking_reservation(
+                               [
+                                       $id,
+                                       {
+                                               flesh => 1,
+                                               flesh_fields => { 
+                                                       bresv => [ "billings", "payments", "pickup_lib" ] }
+                                       }
+                               ]
+                       )
+               );
+       }
+
+       return \@data;
+}
+
 
 
 # --------------------------------------------------------------
index 6cd94b2..b7c8ea7 100644 (file)
@@ -291,9 +291,12 @@ sub retrieve_payable_balance {
             my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event;
             next unless grep { $_ == $circ->circ_lib } @credit_orgs;
 
-        } else {
+        } elsif ($xact->xact_type eq 'grocery') {
             my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event;
             next unless grep { $_ == $bill->billing_location } @credit_orgs;
+        } elsif ($xact->xact_type eq 'reservation') {
+            my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event;
+            next unless grep { $_ == $bill->pickup_lib } @credit_orgs;
         }
         $sum += $xact->balance_owed();
     }
index 1388d81..419179c 100644 (file)
@@ -5,6 +5,7 @@ use Class::DBI::AbstractSearch;
 
 use OpenILS::Application::Storage::CDBI::actor;
 use OpenILS::Application::Storage::CDBI::action;
+use OpenILS::Application::Storage::CDBI::booking;
 use OpenILS::Application::Storage::CDBI::asset;
 use OpenILS::Application::Storage::CDBI::authority;
 use OpenILS::Application::Storage::CDBI::biblio;
@@ -555,6 +556,15 @@ sub modify_from_fieldmapper {
 
        action::circulation->has_a( usr => 'actor::user' );
        actor::user->has_many( circulations => 'action::circulation' => 'usr' );
+
+       booking::resource_attr_map->has_a( resource => 'booking::resource' );
+
+       booking::resource->has_a( owner => 'actor::org_unit' );
+       booking::resource->has_a( type => 'booking::resource_type' );
+       booking::resource_type->has_a( owner => 'actor::org_unit' );
+
+       booking::reservation->has_a( usr => 'actor::user' );
+       actor::user->has_many( reservations => 'booking::reservation' => 'usr' );
        
        action::circulation->has_a( circ_staff => 'actor::user' );
        actor::user->has_many( performed_circulations => 'action::circulation' => 'circ_staff' );
@@ -565,6 +575,8 @@ sub modify_from_fieldmapper {
        action::circulation->has_a( target_copy => 'asset::copy' );
        asset::copy->has_many( circulations => 'action::circulation' => 'target_copy' );
 
+       booking::reservation->has_a( pickup_lib => 'actor::org_unit' );
+
        action::circulation->has_a( circ_lib => 'actor::org_unit' );
        actor::org_unit->has_many( circulations => 'action::circulation' => 'circ_lib' );
        
@@ -621,9 +633,11 @@ sub modify_from_fieldmapper {
        action::circulation->has_many( billings => 'money::billing' => 'xact' );
        action::circulation->has_many( payments => 'money::payment' => 'xact' );
        #action::circulation->might_have( billable_transaction => 'money::billable_transaction' );
-
        #action::open_circulation->might_have( circulation => 'action::circulation' );
 
+       booking::reservation->has_many( billings => 'money::billing' => 'xact' );
+       booking::reservation->has_many( payments => 'money::payment' => 'xact' );
+
        action::in_house_use->has_a( org_unit => 'actor::org_unit' );
        action::in_house_use->has_a( staff => 'actor::user' );
        action::in_house_use->has_a( item => 'asset::copy' );
index c8335b4..0d3dceb 100644 (file)
@@ -127,6 +127,16 @@ __PACKAGE__->columns(Essential => qw/source dest persistant_transfer target_copy
 
 #-------------------------------------------------------------------------------
 
+package action::reservation_transit_copy;
+use base qw/action/;
+__PACKAGE__->table('action_reservation_transit_copy');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/source dest persistant_transfer target_copy
+                                    source_send_time dest_recv_time prev_hop prev_dest
+                                    copy_status reservation/);
+
+#-------------------------------------------------------------------------------
+
 package action::transit_copy;
 use base qw/action/;
 __PACKAGE__->table('action_transit_copy');
diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm
new file mode 100644 (file)
index 0000000..e2b60fa
--- /dev/null
@@ -0,0 +1,57 @@
+package OpenILS::Application::Storage::CDBI::booking;
+our $VERSION = 1;
+
+#-------------------------------------------------------------------------------
+package booking;
+use base qw/OpenILS::Application::Storage::CDBI/;
+#-------------------------------------------------------------------------------
+
+package booking::resource_type;
+use base qw/booking/;
+__PACKAGE__->table('booking_resource_type');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/name fine_interval fine_amount
+                     max_fine owner catalog_item record transferable elbow_room/);
+
+#-------------------------------------------------------------------------------
+
+package booking::resource;
+use base qw/booking/;
+__PACKAGE__->table('booking_resource');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/owner type overbook barcode deposit
+                     deposit_amount user_fee/);
+
+#-------------------------------------------------------------------------------
+
+package booking::reservation;
+use base qw/booking/;
+__PACKAGE__->table('booking_reservation');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/xact_start usr current_resource
+                                    fine_amount max_fine fine_interval xact_finish 
+                                    capture_staff pickup_lib request_time start_time end_time
+                     capture_time cancel_time pickup_time return_time
+                     booking_interval target_resource_type target_resource
+                     current_resource request_lib/);
+
+#-------------------------------------------------------------------------------
+
+package booking::resource_attr_map;
+use base qw/booking/;
+__PACKAGE__->table('booking_resource_attr_map');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/resource resource_attr value/);
+
+#-------------------------------------------------------------------------------
+
+package booking::reservation_attr_value_map;
+use base qw/booking/;
+__PACKAGE__->table('booking_reservation_attr_value_map');
+__PACKAGE__->columns(Primary => 'id');
+__PACKAGE__->columns(Essential => qw/reservation attr_value/);
+
+#-------------------------------------------------------------------------------
+
+1;
+
index 3bb16d9..12deac5 100644 (file)
        action::circulation->sequence( 'money.billable_xact_id_seq' );
 
        #---------------------------------------------------------------------
+       package booking::resource_type;
+       
+       booking::resource_type->table( 'booking.resource_type' );
+       booking::resource_type->sequence( 'booking.resource_type_id_seq' );
+
+       #---------------------------------------------------------------------
+       package booking::resource;
+       
+       booking::resource->table( 'booking.resource' );
+       booking::resource->sequence( 'booking.resource_id_seq' );
+
+       #---------------------------------------------------------------------
+       package booking::reservation;
+       
+       booking::reservation->table( 'booking.reservation' );
+       booking::reservation->sequence( 'money.billable_xact_id_seq' );
+
+       #---------------------------------------------------------------------
+       package booking::reservation_attr_value_map;
+       
+       booking::reservation_attr_value_map->table( 'booking.reservation_attr_value_map' );
+       booking::reservation_attr_value_map->sequence( 'booking.reservation_attr_value_map_id_seq' );
+
+       #---------------------------------------------------------------------
+       package booking::resource_attr_map;
+       
+       booking::resource_attr_map->table( 'booking.resource_attr_map' );
+       booking::resource_attr_map->sequence( 'booking.resource_attr_map_id_seq' );
+
+       #---------------------------------------------------------------------
        package action::non_cat_in_house_use;
        
        action::non_cat_in_house_use->table( 'action.non_cat_in_house_use' );
 
        #-------------------------------------------------------------------------------
 
+       package action::reservation_transit_copy;
+
+       action::reservation_transit_copy->sequence( 'action.transit_copy_id_seq' );
+       action::reservation_transit_copy->table('action.reservation_transit_copy');
+
+       #-------------------------------------------------------------------------------
+
        package action::transit_copy;
 
        action::transit_copy->sequence( 'action.transit_copy_id_seq' );
index 8106dd9..9ac0252 100644 (file)
@@ -114,8 +114,24 @@ sub overdue_circs {
        my $sth = action::circulation->db_Main->prepare_cached($sql);
        $sth->execute($upper_interval);
 
-       return ( map { action::circulation->construct($_) } $sth->fetchall_hash );
+       my @circs = map { action::circulation->construct($_) } $sth->fetchall_hash;
 
+       $c_t = booking::reservation->table;
+       $sql = <<"      SQL";
+               SELECT  *
+                 FROM  $c_t
+                 WHERE return_time IS NULL
+                       AND end_time < ( CURRENT_TIMESTAMP $grace)
+            AND fine_interval IS NOT NULL
+            AND cancel_time IS NULL
+       SQL
+
+       $sth = action::circulation->db_Main->prepare_cached($sql);
+       $sth->execute();
+
+    push @circs, map { booking::reservation->construct($_) } $sth->fetchall_hash;
+
+    return @circs;
 }
 
 sub complete_reshelving {
@@ -597,7 +613,9 @@ sub generate_fines {
 
        my @circs;
        if ($circ) {
-               push @circs, action::circulation->search_where( { id => $circ, stop_fines => undef } );
+               push @circs,
+            action::circulation->search_where( { id => $circ, stop_fines => undef } ),
+            booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } );
        } else {
                push @circs, overdue_circs($grace);
        }
@@ -606,7 +624,22 @@ sub generate_fines {
 
        my $penalty = OpenSRF::AppSession->create('open-ils.penalty');
        for my $c (@circs) {
+
+        my $ctype = ref($c);
+        $ctype =~ s/^.*([^:]+)$/$1/o;
        
+        my $due_date_method = 'due_date';
+        my $target_copy_method = 'target_copy';
+        my $circ_lib_method = 'circ_lib';
+        my $recurring_fine_method = 'recurring_fine';
+        if ($ctype eq 'reservation') {
+            $due_date_method = 'end_time';
+            $target_copy_method = 'current_resource';
+            $circ_lib_method = 'pickup_lib';
+            $recurring_fine_method = 'fine_amount';
+            next unless ($c->fine_interval);
+        }
+
                try {
                        if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
                                $log->debug("Cleaning up after previous transaction\n");
@@ -616,7 +649,7 @@ sub generate_fines {
                        $log->info("Processing circ ".$c->id."...\n");
 
 
-                       my $due_dt = $parser->parse_datetime( clense_ISO8601( $c->due_date ) );
+                       my $due_dt = $parser->parse_datetime( clense_ISO8601( $c->$due_date_method ) );
        
                        my $due = $due_dt->epoch;
                        my $now = time;
@@ -636,15 +669,15 @@ sub generate_fines {
                        }
        
                        $client->respond(
-                               "ARG! Overdue circulation ".$c->id.
-                               " for item ".$c->target_copy.
+                               "ARG! Overdue $ctype ".$c->id.
+                               " for item ".$c->$target_copy_method.
                                " (user ".$c->usr.").\n".
                                "\tItem was due on or before: ".localtime($due)."\n");
        
                        my @fines = money::billing->search_where(
                                { xact => $c->id,
                                  btype => 1,
-                                 billing_ts => { '>' => $c->due_date } },
+                                 billing_ts => { '>' => $c->$due_date_method } },
                                { order_by => 'billing_ts DESC'}
                        );
 
@@ -666,7 +699,7 @@ sub generate_fines {
                                $last_fine = $due;
 
                                if (0) {
-                                       if (my $h = $hoo{$c->circ_lib}) { 
+                                       if (my $h = $hoo{$c->$circ_lib_method}) { 
 
                                                $log->info( "Circ lib has an hours-of-operation entry" );
                                                # find the day after the due date...
@@ -714,17 +747,17 @@ sub generate_fines {
        
                        $client->respond( "\t$pending_fine_count pending fine(s)\n" );
 
-                       my $recuring_fine = int($c->recuring_fine * 100);
+                       my $recurring_fine = int($c->$recurring_fine_method * 100);
                        my $max_fine = int($c->max_fine * 100);
 
                        my ($latest_billing_ts, $latest_amount) = ('',0);
                        for (my $bill = 1; $bill <= $pending_fine_count; $bill++) {
        
                                if ($current_fine_total >= $max_fine) {
-                                       $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'});
+                                       $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'}) if ($ctype eq 'circulation');
                                        $client->respond(
                                                "\tMaximum fine level of ".$c->max_fine.
-                                               " reached for this circulation.\n".
+                                               " reached for this $ctype.\n".
                                                "\tNo more fines will be generated.\n" );
                                        last;
                                }
@@ -735,7 +768,7 @@ sub generate_fines {
                                my $dow_open = "dow_${dow}_open";
                                my $dow_close = "dow_${dow}_close";
 
-                               if (my $h = $hoo{$c->circ_lib}) {
+                               if (my $h = $hoo{$c->$circ_lib_method}) {
                                        next if ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00');
                                }
 
@@ -743,7 +776,7 @@ sub generate_fines {
                                my @cl = actor::org_unit::closed_date->search_where(
                                                { close_start   => { '<=' => $timestamptz },
                                                  close_end     => { '>=' => $timestamptz },
-                                                 org_unit      => $c->circ_lib }
+                                                 org_unit      => $c->$circ_lib_method }
                                );
                                next if (@cl);
        
@@ -772,7 +805,7 @@ sub generate_fines {
 
                 # Caluclate penalties inline
                                OpenILS::Utils::Penalty->calculate_penalties(
-                                       undef, $c->usr->to_fieldmapper->id.'', $c->circ_lib->to_fieldmapper->id.'');
+                                       undef, $c->usr->to_fieldmapper->id.'', $c->$circ_lib_method->to_fieldmapper->id.'');
 
                        } else {
 
@@ -783,7 +816,7 @@ sub generate_fines {
                                $penalty->request(
                                    'open-ils.penalty.patron_penalty.calculate',
                                    { patronid  => ''.$c->usr,
-                                   context_org => ''.$c->circ_lib,
+                                   context_org => ''.$c->$circ_lib_method,
                                    update      => 1,
                                    background  => 1,
                                    }
@@ -792,8 +825,8 @@ sub generate_fines {
 
                } catch Error with {
                        my $e = shift;
-                       $client->respond( "Error processing overdue circulation [".$c->id."]:\n\n$e\n" );
-                       $log->error("Error processing overdue circulation [".$c->id."]:\n$e\n");
+                       $client->respond( "Error processing overdue $ctype [".$c->id."]:\n\n$e\n" );
+                       $log->error("Error processing overdue $ctype [".$c->id."]:\n$e\n");
                        $self->method_lookup('open-ils.storage.transaction.rollback')->run;
                        throw $e if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o);
                };
@@ -1129,6 +1162,206 @@ __PACKAGE__->register_method(
        method          => 'new_hold_copy_targeter',
 );
 
+sub reservation_targeter {
+       my $self = shift;
+       my $client = shift;
+       my $one_reservation = shift;
+
+       local $OpenILS::Application::Storage::WRITE = 1;
+
+       my $reservations;
+
+       try {
+               if ($one_reservation) {
+                       $self->method_lookup('open-ils.storage.transaction.begin')->run( $client );
+                       $reservations = [ booking::reservation->search_where( { id => $one_reservation, capture_time => undef, cancel_time => undef } ) ];
+               } else {
+
+                       # find all the reservations needing targeting
+                       $reservations = [
+                booking::reservation->search_where(
+                                       { current_resource => undef,
+                                         cancel_time => undef,
+                                         start_time => { '>' => 'now' }
+                    },
+                    { order_by => 'start_time' }
+                )
+            ];
+               }
+       } catch Error with {
+               my $e = shift;
+               die "Could not retrieve reservation requests:\n\n$e\n";
+       };
+
+       my @successes = ();
+       for my $bresv (@$reservations) {
+               try {
+                       #start a transaction if needed
+                       if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
+                               $log->debug("Cleaning up after previous transaction\n");
+                               $self->method_lookup('open-ils.storage.transaction.rollback')->run;
+                       }
+                       $self->method_lookup('open-ils.storage.transaction.begin')->run( $client );
+                       $log->info("Processing reservation ".$bresv->id."...\n");
+
+                       #first, re-fetch the hold, to make sure it's not captured already
+                       $bresv->remove_from_object_index();
+                       $bresv = booking::reservation->retrieve( $bresv->id );
+
+                       die "OK\n" if (!$bresv or $bresv->capture_time or $bresv->cancel_time);
+
+                       my $end_time = $parser->parse_datetime( clense_ISO8601( $bresv->end_time ) );
+                       if (DateTime->compare($end_time, DateTime->now) < 0) {
+
+                               # cancel cause = un-targeted expiration
+                               $bresv->update( { cancel_time => 'now' } ); 
+                               $self->method_lookup('open-ils.storage.transaction.commit')->run;
+
+                               # tell A/T the reservation was cancelled
+                               my $fm_bresv = $bresv->to_fieldmapper;
+                               my $ses = OpenSRF::AppSession->create('open-ils.trigger');
+                               $ses->request('open-ils.trigger.event.autocreate', 
+                                       'booking.reservation.cancel.expire_no_target', $fm_bresv, $fm_bresv->pickup_lib);
+
+                               die "OK\n";
+                       }
+
+                       my $possible_resources;
+
+                       # find all the potential resources
+                       if (!$bresv->target_resource) {
+                               my $filter = { type => $bresv->target_resource_type };
+                               my $attr_maps = [ booking::reservation_attr_value_map->search( reservation => $bresv->id) ];
+
+                               $filter->{attribute_values} = [ map { $_->attr_value } @$attr_maps ] if (@$attr_maps);
+
+                               $filter->{available} = [$bresv->start_time, $bresv->end_time];
+                               my $ses = OpenSRF::AppSession->create('open-ils.booking');
+                               $possible_resources = $ses->request('open-ils.booking.resources.filtered_id_list', undef, $filter)->gather(1);
+                       } else {
+                               $possible_resources = $bresv->target_resource;
+                       }
+
+            my $all_resources = [ booking::resource->search( id => $possible_resources ) ];
+                       @$all_resources = grep { isTrue($_->type->transferable) || $_->owner.'' eq $bresv->pickup_lib.'' } @$all_resources;
+
+
+            my @good_resources = ();
+            for my $res (@$all_resources) {
+                unless (isTrue($res->type->catalog_item)) {
+                    push @good_resources, $res;
+                    next;
+                }
+
+                my $copy = [ asset::copy->search( deleted => f, barcode => $res->barcode )]->[0];
+
+                unless ($copy) {
+                    push @good_resources, $res;
+                    next;
+                }
+
+                if ($copy->status->id == 0 || $copy->status->id == 7) {
+                    push @good_resources, $res;
+                    next;
+                }
+
+                if ($copy->status->id == 1) {
+                    my $circs = action::circulation->search_where(
+                        {target_copy => $copy->id, checkin_time => undef },
+                        { order_by => 'id DESC' }
+                    );
+
+                    if (@$circs) {
+                        my $due_date = $circs->[0]->due_date;
+                                   $due_date = $parser->parse_datetime( clense_ISO8601( $due_date ) );
+                                   my $start_time = $parser->parse_datetime( clense_ISO8601( $bresv->start_time ) );
+                        next if (DateTime->compare($start_time, $due_date) < 0);
+                        push @good_resources, $res;
+                    }
+
+                    next;
+                }
+
+                push @good_resources, $res if (isTrue($copy->status->holdable));
+            }
+
+                       # let 'em know we're still working
+                       $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
+                       
+                       # if we have no copies ...
+                       if (!@good_resources) {
+                               $log->info("\tNo resources available for targeting at all!\n");
+                               push @successes, { reservation => $bresv->id, eligible_copies => 0, error => 'NO_COPIES' };
+
+                               $self->method_lookup('open-ils.storage.transaction.commit')->run;
+                               die "OK\n";
+                       }
+
+                       $log->debug("\t".scalar(@good_resources)." resources available for targeting...");
+
+                       my $prox_list = [];
+                       $$prox_list[0] =
+                       [
+                               grep {
+                                       $_->owner == $bresv->pickup_lib
+                               } @good_resources
+                       ];
+
+                       $all_resources = [grep {$_->owner != $bresv->pickup_lib } @good_resources];
+                       # $all_copies is now a list of copies not at the pickup library
+
+                       my $best = shift @good_resources;
+                       $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
+
+                       if (!$best) {
+                               $log->debug("\tNothing at the pickup lib, looking elsewhere among ".scalar(@$all_resources)." resources");
+
+                               $prox_list =
+                    map  { $_->[1] }
+                    sort { $a->[0] <> $b->[0] }
+                    map  {
+                        [   actor::org_unit_proximity->search_where(
+                                { from_org => $bresv->pickup_lib.'', to_org => $_=>owner.'' }
+                            )->[0]->prox,
+                            $_
+                        ]
+                    } @$all_resources;
+
+                               $client->status( new OpenSRF::DomainObject::oilsContinueStatus );
+
+                               $best = shift @$prox_list
+                       }
+
+                       if ($best) {
+                               $bresv->update( { current_resource => ''.$best->id } );
+                               $log->debug("\tUpdating reservation [".$bresv->id."] with new 'current_resource' [".$best->id."] for reservation fulfillment.");
+                       }
+
+                       $self->method_lookup('open-ils.storage.transaction.commit')->run;
+                       $log->info("\tProcessing of bresv ".$bresv->id." complete.");
+
+                       push @successes,
+                               { reservation => $bresv->id,
+                                 current_resource => ($best ? $best->id : undef) };
+
+               } otherwise {
+                       my $e = shift;
+                       if ($e !~ /^OK/o) {
+                               $log->error("Processing of bresv failed:  $e");
+                               $self->method_lookup('open-ils.storage.transaction.rollback')->run;
+                               throw $e if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o);
+                       }
+               };
+       }
+
+       return \@successes;
+}
+__PACKAGE__->register_method(
+       api_name        => 'open-ils.storage.booking.reservation.resource_targeter',
+       api_level       => 1,
+       method          => 'reservation_targeter',
+);
+
 my $locations;
 my $statuses;
 my %cache = (titles => {}, cns => {});
index b049a0a..e1a2f0a 100644 (file)
@@ -51,8 +51,13 @@ sub _make_mbts {
                 $s->balance_owed( sprintf('%0.2f', (($to) - ($tp)) / 100) );
                #$log->debug( "balance of ".$x->id." == ".$s->balance_owed, DEBUG );
 
-                $s->xact_type( 'grocery' ) if (money::grocery->retrieve($x->id));
-                $s->xact_type( 'circulation' ) if (action::circulation->retrieve($x->id));
+                if (action::circulation->retrieve($x->id)) {
+                    $s->xact_type( 'circulation' );
+                } elsif (money::grocery->retrieve($x->id)) {
+                    $s->xact_type( 'grocery' );
+                } elsif (booking::reservation->retrieve($x->id)) {
+                    $s->xact_type( 'reservation' );
+                }
 
                 push @mbts, $s;
         }
@@ -117,6 +122,7 @@ sub new_collections {
        my $mb = money::billing->table;
        my $circ = action::circulation->table;
        my $mg = money::grocery->table;
+       my $res = booking::reservation->table;
        my $descendants = "actor.org_unit_descendants((select id from actor.org_unit where shortname = ?))";
 
        my $SQL = <<"   SQL";
@@ -156,6 +162,23 @@ select
                 and b.billing_ts < current_timestamp - ? * '1 day'::interval
                 and not b.voided
           group by 1,2
+
+                  union all
+
+         select
+                x.id,
+                x.usr,
+                MAX(b.billing_ts) as last_billing,
+                SUM(b.amount) AS total_billing
+          from  booking.reservation x
+                left join money.collections_tracker c ON (c.usr = x.usr AND c.location = ?)
+                join money.billing b on (b.xact = x.id)
+          where x.xact_finish is null
+                and c.id is null
+                and x.pickup_lib in (XX)
+                and b.billing_ts < current_timestamp - ? * '1 day'::interval
+                and not b.voided
+          group by 1,2
         ) full_list
         left join money.payment p on (full_list.id = p.xact)
   group by 1
@@ -236,6 +259,20 @@ select
                 and b.billing_ts between ? and ?
                 and not b.voided
           group by 1,2
+
+                  union all
+
+         select
+                x.id,
+                x.usr,
+                SUM(b.amount) AS total_billing
+          from  booking.reservation x
+                join money.billing b on (b.xact = x.id)
+          where x.xact_finish is null
+                and x.pickup_lib in (XX)
+                and b.billing_ts between ? and ?
+                and not b.voided
+          group by 1,2
         ) full_list
         left join money.payment p on (full_list.id = p.xact)
   group by 1
@@ -292,6 +329,42 @@ SELECT  usr,
                 SELECT  lt.usr,
                         NULL::TIMESTAMPTZ AS last_pertinent_billing,
                         NULL::TIMESTAMPTZ AS last_pertinent_payment
+                  FROM  booking.reservation lt
+                        JOIN money.collections_tracker cl ON (lt.usr = cl.usr)
+                        JOIN money.billing bl ON (lt.id = bl.xact)
+                  WHERE cl.location = ?
+                        AND lt.pickup_lib IN (XX)
+                        AND bl.void_time BETWEEN ? AND ?
+                  GROUP BY 1
+
+                                UNION ALL
+                SELECT  lt.usr,
+                        MAX(bl.billing_ts) AS last_pertinent_billing,
+                        NULL::TIMESTAMPTZ AS last_pertinent_payment
+                  FROM  booking.reservation lt
+                        JOIN money.collections_tracker cl ON (lt.usr = cl.usr)
+                        JOIN money.billing bl ON (lt.id = bl.xact)
+                  WHERE cl.location = ?
+                        AND lt.pickup_lib IN (XX)
+                        AND bl.billing_ts BETWEEN ? AND ?
+                  GROUP BY 1
+
+                                UNION ALL
+                SELECT  lt.usr,
+                        NULL::TIMESTAMPTZ AS last_pertinent_billing,
+                        MAX(pm.payment_ts) AS last_pertinent_payment
+                  FROM  booking.reservation lt
+                        JOIN money.collections_tracker cl ON (lt.usr = cl.usr)
+                        JOIN money.payment pm ON (lt.id = pm.xact)
+                  WHERE cl.location = ?
+                        AND lt.pickup_lib IN (XX)
+                        AND pm.payment_ts BETWEEN ? AND ?
+                  GROUP BY 1
+
+                                UNION ALL
+                 SELECT  lt.usr,
+                        NULL::TIMESTAMPTZ AS last_pertinent_billing,
+                        NULL::TIMESTAMPTZ AS last_pertinent_payment
                   FROM  money.grocery lt
                         JOIN money.collections_tracker cl ON (lt.usr = cl.usr)
                         JOIN money.billing bl ON (lt.id = bl.xact)
@@ -389,9 +462,17 @@ SELECT  usr,
 
                my $sth = money::collections_tracker->db_Main->prepare($real_sql);
                $sth->execute(
+            # reservation queries
                        $org->id, $startdate, $enddate,
                        $org->id, $startdate, $enddate,
                        $org->id, $startdate, $enddate,
+
+            # grocery queries
+                       $org->id, $startdate, $enddate,
+                       $org->id, $startdate, $enddate,
+                       $org->id, $startdate, $enddate,
+
+            # circ queries
                        $org->id, $startdate, $enddate,
                        $org->id, $startdate, $enddate,
                        $org->id, $startdate, $enddate,
index 5d58ef6..624c9fd 100644 (file)
@@ -41,6 +41,7 @@ econst OILS_COPY_STATUS_CATALOGING    => 11;
 econst OILS_COPY_STATUS_RESERVES      => 12;
 econst OILS_COPY_STATUS_DISCARD       => 13;
 econst OILS_COPY_STATUS_DAMAGED       => 14;
+econst OILS_COPY_STATUS_ON_RESV_SHELF => 15;
 
 
 # ---------------------------------------------------------------------
index 8881e2f..782b085 100644 (file)
@@ -258,7 +258,7 @@ sub set_savepoint {
     my $name = shift || 'savepoint';
     return unless $self->{session} and $self->{xact_id};
        $self->log(I, "setting savepoint '$name'");
-       my $stat = $self->request($self->app.".savepoint.set")
+       my $stat = $self->request($self->app.".savepoint.set", $name)
            or $self->log(E, "error setting savepoint '$name'");
     return $stat;
 }
@@ -268,7 +268,7 @@ sub release_savepoint {
     my $name = shift || 'savepoint';
     return unless $self->{session} and $self->{xact_id};
        $self->log(I, "releasing savepoint '$name'");
-       my $stat = $self->request($self->app.".savepoint.release")
+       my $stat = $self->request($self->app.".savepoint.release", $name)
         or $self->log(E, "error releasing savepoint '$name'");
     return $stat;
 }
@@ -278,7 +278,7 @@ sub rollback_savepoint {
     my $name = shift || 'savepoint';
     return unless $self->{session} and $self->{xact_id};
        $self->log(I, "rollback savepoint '$name'");
-       my $stat = $self->request($self->app.".savepoint.rollback")
+       my $stat = $self->request($self->app.".savepoint.rollback", $name)
         or $self->log(E, "error rolling back savepoint '$name'");
     return $stat;
 }
index a5015ee..cb7c322 100644 (file)
@@ -97,9 +97,17 @@ sub handler {
             my $s = $cstore->request('open-ils.cstore.direct.money.billable_xact_summary.retrieve' => $xact)->gather(1);
             my $u = $cstore->request('open-ils.cstore.direct.actor.usr.retrieve' => $s->usr)->gather(1);
             my $c = $cstore->request('open-ils.cstore.direct.actor.card.retrieve' => $u->card)->gather(1);
-            my $w = $s->xact_type eq 'circulation' ? 
-                $cstore->request('open-ils.cstore.direct.action.circulation.retrieve' => $xact)->gather(1)->circ_lib :
-                $cstore->request('open-ils.cstore.direct.money.grocery.retrieve' => $xact)->gather(1)->billing_location;
+            my $w;
+
+            if ($s->xact_type eq 'circulation') {
+                $w = $cstore->request('open-ils.cstore.direct.action.circulation.retrieve' => $xact)->gather(1)->circ_lib :
+            } elsif ($s->xact_type eq 'grocery') {
+                $w = $cstore->request('open-ils.cstore.direct.money.grocery.retrieve' => $xact)->gather(1)->billing_location;
+            } elsif ($s->xact_type eq 'reservation') {
+                $w = $cstore->request('open-ils.cstore.direct.booking.reservation.retrieve' => $xact)->gather(1)->pickup_lib;
+            } else {
+                die;
+            }
     
             my $failures = $actor->request('open-ils.actor.user.perm.check', $auth_ses, $user->id, $w, ['MARK_BAD_DEBT'])->gather(1);
     
index dc294cc..1033b6b 100644 (file)
@@ -51,7 +51,7 @@ CREATE TABLE config.upgrade_log (
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0131'); -- dbs
+INSERT INTO config.upgrade_log (version) VALUES ('0130'); -- senator
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/095.schema.booking.sql b/Open-ILS/src/sql/Pg/095.schema.booking.sql
new file mode 100644 (file)
index 0000000..f79d968
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2009  Equinox Software, Inc.
+ * Scott McKellar <scott@esilibrary.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+BEGIN;
+
+DROP SCHEMA IF EXISTS booking CASCADE;
+
+CREATE SCHEMA booking;
+
+CREATE TABLE booking.resource_type (
+       id             SERIAL          PRIMARY KEY,
+       name           TEXT            NOT NULL,
+       elbow_room     INTERVAL,
+       fine_interval  INTERVAL,
+       fine_amount    DECIMAL(8,2)    NOT NULL DEFAULT 0,
+       max_fine       DECIMAL(8,2),
+       owner          INT             NOT NULL
+                                      REFERENCES actor.org_unit( id )
+                                      DEFERRABLE INITIALLY DEFERRED,
+       catalog_item   BOOLEAN         NOT NULL DEFAULT FALSE,
+       transferable   BOOLEAN         NOT NULL DEFAULT FALSE,
+    record         INT             REFERENCES biblio.record_entry (id)
+                                   DEFERRABLE INITIALLY DEFERRED,
+       CONSTRAINT brt_name_once_per_owner UNIQUE(owner, name, record)
+);
+
+CREATE TABLE booking.resource (
+       id             SERIAL           PRIMARY KEY,
+       owner          INT              NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       type           INT              NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       overbook       BOOLEAN          NOT NULL DEFAULT FALSE,
+       barcode        TEXT             NOT NULL,
+       deposit        BOOLEAN          NOT NULL DEFAULT FALSE,
+       deposit_amount DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
+       user_fee       DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
+       CONSTRAINT br_unique UNIQUE(owner, barcode)
+);
+
+-- For non-catalog items: hijack barcode for name/description
+
+CREATE TABLE booking.resource_attr (
+       id              SERIAL          PRIMARY KEY,
+       owner           INT             NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       name            TEXT            NOT NULL,
+       resource_type   INT             NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       required        BOOLEAN         NOT NULL DEFAULT FALSE,
+       CONSTRAINT bra_name_once_per_type UNIQUE(resource_type, name)
+);
+
+CREATE TABLE booking.resource_attr_value (
+       id               SERIAL         PRIMARY KEY,
+       owner            INT            NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       attr             INT            NOT NULL
+                                       REFERENCES booking.resource_attr(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       valid_value      TEXT           NOT NULL,
+       CONSTRAINT brav_logical_key UNIQUE(owner, attr, valid_value)
+);
+
+-- Do we still need a name column?
+
+
+CREATE TABLE booking.resource_attr_map (
+       id               SERIAL         PRIMARY KEY,
+       resource         INT            NOT NULL
+                                       REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       resource_attr    INT            NOT NULL
+                                       REFERENCES booking.resource_attr(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       value            INT            NOT NULL
+                                       REFERENCES booking.resource_attr_value(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       CONSTRAINT bram_one_value_per_attr UNIQUE(resource, resource_attr)
+);
+
+CREATE TABLE booking.reservation (
+       request_time     TIMESTAMPTZ   NOT NULL DEFAULT now(),
+       start_time       TIMESTAMPTZ,
+       end_time         TIMESTAMPTZ,
+       capture_time     TIMESTAMPTZ,
+       cancel_time      TIMESTAMPTZ,
+       pickup_time      TIMESTAMPTZ,
+       return_time      TIMESTAMPTZ,
+       booking_interval INTERVAL,
+       fine_interval    INTERVAL,
+       fine_amount      DECIMAL(8,2),
+       max_fine         DECIMAL(8,2),
+       target_resource_type  INT       NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       target_resource  INT            REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       current_resource INT            REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       request_lib      INT            NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       pickup_lib       INT            REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       capture_staff    INT            REFERENCES actor.usr(id)
+                                       DEFERRABLE INITIALLY DEFERRED
+) INHERITS (money.billable_xact);
+
+ALTER TABLE booking.reservation ADD PRIMARY KEY (id);
+
+ALTER TABLE booking.reservation
+       ADD CONSTRAINT booking_reservation_usr_fkey
+       FOREIGN KEY (usr) REFERENCES actor.usr (id)
+       DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('reservation');
+CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
+CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
+
+
+CREATE TABLE booking.reservation_attr_value_map (
+       id               SERIAL         PRIMARY KEY,
+       reservation      INT            NOT NULL
+                                       REFERENCES booking.reservation(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       attr_value       INT            NOT NULL
+                                       REFERENCES booking.resource_attr_value(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       CONSTRAINT bravm_logical_key UNIQUE(reservation, attr_value)
+);
+
+CREATE TABLE action.reservation_transit_copy (
+    reservation    INT REFERENCES booking.reservation (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+) INHERITS (action.transit_copy);
+ALTER TABLE action.reservation_transit_copy ADD PRIMARY KEY (id);
+ALTER TABLE action.reservation_transit_copy ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy) REFERENCES booking.resource (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+CREATE INDEX active_reservation_transit_dest_idx ON "action".reservation_transit_copy (dest);
+CREATE INDEX active_reservation_transit_source_idx ON "action".reservation_transit_copy (source);
+CREATE INDEX active_reservation_transit_cp_idx ON "action".reservation_transit_copy (target_copy);
+
+COMMIT;
index 9eb1d5b..06558e4 100644 (file)
@@ -431,6 +431,12 @@ BEGIN
         SELECT  SUM(f.balance_owed) INTO current_fines
           FROM  money.materialized_billable_xact_summary f
                 JOIN (
+                    SELECT  r.id
+                      FROM  booking.reservation r
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
                     SELECT  g.id
                       FROM  money.grocery g
                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
@@ -611,6 +617,12 @@ BEGIN
         SELECT  SUM(f.balance_owed) INTO current_fines
           FROM  money.materialized_billable_xact_summary f
                 JOIN (
+                    SELECT  r.id
+                      FROM  booking.reservation r
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
                     SELECT  g.id
                       FROM  money.grocery g
                             JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
index fbc46ff..4909e82 100644 (file)
@@ -20,7 +20,7 @@ BEGIN;
 CREATE OR REPLACE VIEW money.open_billable_xact_summary AS
        SELECT  xact.id AS id,
                xact.usr AS usr,
-               COALESCE(circ.circ_lib,groc.billing_location) AS billing_location,
+               COALESCE(circ.circ_lib,groc.billing_location,res.pickup_lib) AS billing_location,
                xact.xact_start AS xact_start,
                xact.xact_finish AS xact_finish,
                SUM(credit.amount) AS total_paid,
@@ -37,6 +37,7 @@ CREATE OR REPLACE VIEW money.open_billable_xact_summary AS
                JOIN pg_class p ON (xact.tableoid = p.oid)
                LEFT JOIN "action".circulation circ ON (circ.id = xact.id)
                LEFT JOIN money.grocery groc ON (groc.id = xact.id)
+               LEFT JOIN booking.reservation res ON (groc.id = xact.id)
                LEFT JOIN (
                        SELECT  billing.xact,
                                billing.voided,
index 502b0e4..63045fb 100644 (file)
@@ -126,6 +126,7 @@ INSERT INTO config.copy_status (id,name) VALUES (11,oils_i18n_gettext(11, 'Catal
 INSERT INTO config.copy_status (id,name,opac_visible) VALUES (12,oils_i18n_gettext(12, 'Reserves', 'ccs', 'name'),'t');
 INSERT INTO config.copy_status (id,name) VALUES (13,oils_i18n_gettext(13, 'Discard/Weed', 'ccs', 'name'));
 INSERT INTO config.copy_status (id,name) VALUES (14,oils_i18n_gettext(14, 'Damaged', 'ccs', 'name'));
+INSERT INTO config.copy_status (id,name) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name'));
 
 SELECT SETVAL('config.copy_status_id_seq'::TEXT, 100);
 
@@ -1231,6 +1232,17 @@ INSERT INTO permission.perm_list VALUES
     (201, 'DELETE_MFHD_RECORD', oils_i18n_gettext(201, 'Allows a user to delete an MFHD record', 'ppl', 'description')),
     (202, 'ADMIN_ACQ_FUND', oils_i18n_gettext(202, 'Allow a user to create/view/update/delete a fund', 'ppl', 'description')),
     (203, 'group_application.user.staff.acq_admin', oils_i18n_gettext(203, 'Allows a user to add/remove/edit users in the "Acquisitions Administrators" group', 'ppl', 'description'))
+    (351, 'HOLD_LOCAL_AVAIL_OVERRIDE', oils_i18n_gettext(351, 'Allow a user to place a hold despite the availability of a local copy', 'ppl', 'description')),
+    (352, 'ADMIN_BOOKING_RESOURCE', oils_i18n_gettext(352, 'Enables the user to create/update/delete booking resources', 'ppl', 'description')),
+    (353, 'ADMIN_BOOKING_RESOURCE_TYPE', oils_i18n_gettext(353, 'Enables the user to create/update/delete booking resource types', 'ppl', 'description')),
+    (354, 'ADMIN_BOOKING_RESOURCE_ATTR', oils_i18n_gettext(354, 'Enables the user to create/update/delete booking resource attributes', 'ppl', 'description')),
+    (355, 'ADMIN_BOOKING_RESOURCE_ATTR_MAP', oils_i18n_gettext(355, 'Enables the user to create/update/delete booking resource attribute maps', 'ppl', 'description')),
+    (356, 'ADMIN_BOOKING_RESOURCE_ATTR_VALUE', oils_i18n_gettext(356, 'Enables the user to create/update/delete booking resource attribute values', 'ppl', 'description')),
+    (357, 'ADMIN_BOOKING_RESERVATION', oils_i18n_gettext(357, 'Enables the user to create/update/delete booking reservations', 'ppl', 'description')),
+    (358, 'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP', oils_i18n_gettext(358, 'Enables the user to create/update/delete booking reservation attribute value maps', 'ppl', 'description')),
+    (359, 'HOLD_ITEM_CHECKED_OUT.override', oils_i18n_gettext(359, 'Allows a user to place a hold on an item that they already have checked out', 'ppl', 'description')),
+    (360, 'RETRIEVE_RESERVATION_PULL_LIST', oils_i18n_gettext(360, 'Allows a user to retrieve a booking reservation pull list', 'ppl', 'description')),
+    (361, 'CAPTURE_RESERVATION', oils_i18n_gettext(361, 'Allows a user to capture booking reservations', 'ppl', 'description'))
 ;
 
 SELECT SETVAL('permission.perm_list_id_seq'::TEXT, (SELECT MAX(id) FROM permission.perm_list));
@@ -1379,7 +1391,7 @@ INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.cat.bib
 INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.cat.bib.alert_on_empty');
 INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.patron.password.use_phone');
 
-
+SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
 
 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
        (1, oils_i18n_gettext(1, 'Users', 'pgt', 'name'), NULL, NULL, '3 years', FALSE, 'group_application.user');
index 705e770..a6642a0 100755 (executable)
@@ -89,15 +89,18 @@ ordered_file_list="
 
   005.schema.actors.sql
   006.schema.permissions.sql
+  008.schema.query.sql
   010.schema.biblio.sql
   011.schema.authority.sql
   012.schema.vandelay.sql
+  015.schema.staging.sql
   020.schema.functions.sql
   030.schema.metabib.sql
   040.schema.asset.sql
   070.schema.container.sql
   080.schema.money.sql
   090.schema.action.sql
+  095.schema.booking.sql
   
   100.circ_matrix.sql
   110.hold_matrix.sql
diff --git a/Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql b/Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql
new file mode 100644 (file)
index 0000000..112cd71
--- /dev/null
@@ -0,0 +1,135 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0086');
+
+DROP SCHEMA IF EXISTS booking CASCADE;
+
+CREATE SCHEMA booking;
+
+CREATE TABLE booking.resource_type (
+       id             SERIAL          PRIMARY KEY,
+       name           TEXT            NOT NULL,
+       fine_interval  INTERVAL,
+       fine_amount    DECIMAL(8,2)    NOT NULL DEFAULT 0,
+       owner          INT             NOT NULL
+                                      REFERENCES actor.org_unit( id )
+                                      DEFERRABLE INITIALLY DEFERRED,
+       catalog_item   BOOLEAN         NOT NULL DEFAULT FALSE,
+       transferable   BOOLEAN         NOT NULL DEFAULT FALSE,
+       CONSTRAINT brt_name_once_per_owner UNIQUE(owner, name)
+);
+
+CREATE TABLE booking.resource (
+       id             SERIAL           PRIMARY KEY,
+       owner          INT              NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       type           INT              NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       overbook       BOOLEAN          NOT NULL DEFAULT FALSE,
+       barcode        TEXT             NOT NULL,
+       deposit        BOOLEAN          NOT NULL DEFAULT FALSE,
+       deposit_amount DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
+       user_fee       DECIMAL(8,2)     NOT NULL DEFAULT 0.00,
+       CONSTRAINT br_unique UNIQUE(owner, type, barcode)
+);
+
+-- For non-catalog items: hijack barcode for name/description
+
+CREATE TABLE booking.resource_attr (
+       id              SERIAL          PRIMARY KEY,
+       owner           INT             NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       name            TEXT            NOT NULL,
+       resource_type   INT             NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       required        BOOLEAN         NOT NULL DEFAULT FALSE,
+       CONSTRAINT bra_name_once_per_type UNIQUE(resource_type, name)
+);
+
+CREATE TABLE booking.resource_attr_value (
+       id               SERIAL         PRIMARY KEY,
+       owner            INT            NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       attr             INT            NOT NULL
+                                       REFERENCES booking.resource_attr(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       valid_value      TEXT           NOT NULL,
+       CONSTRAINT brav_logical_key UNIQUE(owner, attr, valid_value)
+);
+
+-- Do we still need a name column?
+
+
+CREATE TABLE booking.resource_attr_map (
+       id               SERIAL         PRIMARY KEY,
+       resource         INT            NOT NULL
+                                       REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       resource_attr    INT            NOT NULL
+                                       REFERENCES booking.resource_attr(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       value            INT            NOT NULL
+                                       REFERENCES booking.resource_attr_value(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       CONSTRAINT bram_one_value_per_attr UNIQUE(resource, resource_attr)
+);
+
+CREATE TABLE booking.reservation (
+       request_time     TIMESTAMPTZ   NOT NULL DEFAULT now(),
+       start_time       TIMESTAMPTZ,
+       end_time         TIMESTAMPTZ,
+       capture_time     TIMESTAMPTZ,
+       cancel_time      TIMESTAMPTZ,
+       pickup_time      TIMESTAMPTZ,
+       return_time      TIMESTAMPTZ,
+       booking_interval INTERVAL,
+       fine_interval    INTERVAL,
+       fine_amount      DECIMAL(8,2),
+       target_resource_type  INT       NOT NULL
+                                       REFERENCES booking.resource_type(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       target_resource  INT            REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       current_resource INT            REFERENCES booking.resource(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       request_lib      INT            NOT NULL
+                                       REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       pickup_lib       INT            REFERENCES actor.org_unit(id)
+                                       DEFERRABLE INITIALLY DEFERRED,
+       capture_staff    INT            REFERENCES actor.usr(id)
+                                       DEFERRABLE INITIALLY DEFERRED
+) INHERITS (money.billable_xact);
+
+ALTER TABLE booking.reservation ADD PRIMARY KEY (id);
+
+ALTER TABLE booking.reservation
+       ADD CONSTRAINT booking_reservation_usr_fkey
+       FOREIGN KEY (usr) REFERENCES actor.usr (id)
+       DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE booking.reservation_attr_value_map (
+       id               SERIAL         PRIMARY KEY,
+       reservation      INT            NOT NULL
+                                       REFERENCES booking.reservation(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       attr_value       INT            NOT NULL
+                                       REFERENCES booking.resource_attr_value(id)
+                                       ON DELETE CASCADE
+                                       DEFERRABLE INITIALLY DEFERRED,
+       CONSTRAINT bravm_logical_key UNIQUE(reservation, attr_value)
+);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql b/Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql
new file mode 100644 (file)
index 0000000..2ee8bb7
--- /dev/null
@@ -0,0 +1,9 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0090'); -- miker
+
+ALTER TABLE booking.resource_type DROP CONSTRAINT brt_name_once_per_owner;
+ALTER TABLE booking.resource_type ADD COLUMN record BIGINT REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE booking.resource_type ADD CONSTRAINT brt_name_or_record_once_per_owner UNIQUE(owner, name, record);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql b/Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql
new file mode 100644 (file)
index 0000000..34dc26b
--- /dev/null
@@ -0,0 +1,8 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0103'); -- miker
+
+ALTER TABLE booking.resource_type ADD COLUMN max_fine NUMERIC(8,2);
+ALTER TABLE booking.reservation ADD COLUMN max_fine NUMERIC(8,2);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql b/Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql
new file mode 100644 (file)
index 0000000..2887048
--- /dev/null
@@ -0,0 +1,339 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0105'); -- miker
+
+CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('reservation');
+CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update ();
+CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete ();
+
+CREATE OR REPLACE VIEW money.open_billable_xact_summary AS
+    SELECT  xact.id AS id,
+        xact.usr AS usr,
+        COALESCE(circ.circ_lib,groc.billing_location,res.pickup_lib) AS billing_location,
+        xact.xact_start AS xact_start,
+        xact.xact_finish AS xact_finish,
+        SUM(credit.amount) AS total_paid,
+        MAX(credit.payment_ts) AS last_payment_ts,
+        LAST(credit.note) AS last_payment_note,
+        LAST(credit.payment_type) AS last_payment_type,
+        SUM(debit.amount) AS total_owed,
+        MAX(debit.billing_ts) AS last_billing_ts,
+        LAST(debit.note) AS last_billing_note,
+        LAST(debit.billing_type) AS last_billing_type,
+        COALESCE(SUM(debit.amount),0) - COALESCE(SUM(credit.amount),0) AS balance_owed,
+        p.relname AS xact_type
+      FROM  money.billable_xact xact
+        JOIN pg_class p ON (xact.tableoid = p.oid)
+        LEFT JOIN "action".circulation circ ON (circ.id = xact.id)
+        LEFT JOIN money.grocery groc ON (groc.id = xact.id)
+        LEFT JOIN booking.reservation res ON (groc.id = xact.id)
+        LEFT JOIN (
+            SELECT  billing.xact,
+                billing.voided,
+                sum(billing.amount) AS amount,
+                max(billing.billing_ts) AS billing_ts,
+                last(billing.note) AS note,
+                last(billing.billing_type) AS billing_type
+              FROM  money.billing
+              WHERE billing.voided IS FALSE
+              GROUP BY billing.xact, billing.voided
+        ) debit ON (xact.id = debit.xact AND debit.voided IS FALSE)
+        LEFT JOIN (
+            SELECT  payment_view.xact,
+                payment_view.voided,
+                sum(payment_view.amount) AS amount,
+                max(payment_view.payment_ts) AS payment_ts,
+                last(payment_view.note) AS note,
+                last(payment_view.payment_type) AS payment_type
+              FROM  money.payment_view
+              WHERE payment_view.voided IS FALSE
+              GROUP BY payment_view.xact, payment_view.voided
+        ) credit ON (xact.id = credit.xact AND credit.voided IS FALSE)
+      WHERE xact.xact_finish IS NULL
+      GROUP BY 1,2,3,4,5,15
+      ORDER BY MAX(debit.billing_ts), MAX(credit.payment_ts);
+
+CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$
+DECLARE
+    user_object         actor.usr%ROWTYPE;
+    new_sp_row          actor.usr_standing_penalty%ROWTYPE;
+    existing_sp_row     actor.usr_standing_penalty%ROWTYPE;
+    collections_fines   permission.grp_penalty_threshold%ROWTYPE;
+    max_fines           permission.grp_penalty_threshold%ROWTYPE;
+    max_overdue         permission.grp_penalty_threshold%ROWTYPE;
+    max_items_out       permission.grp_penalty_threshold%ROWTYPE;
+    tmp_grp             INT;
+    items_overdue       INT;
+    items_out           INT;
+    context_org_list    INT[];
+    current_fines        NUMERIC(8,2) := 0.0;
+    tmp_fines            NUMERIC(8,2);
+    tmp_groc            RECORD;
+    tmp_circ            RECORD;
+    tmp_org             actor.org_unit%ROWTYPE;
+BEGIN
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+
+    -- Max fines
+    SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
+
+    -- Fail if the user has a high fine balance
+    LOOP
+        tmp_grp := user_object.profile;
+        LOOP
+            SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id;
+
+            IF max_fines.threshold IS NULL THEN
+                SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
+            ELSE
+                EXIT;
+            END IF;
+
+            IF tmp_grp IS NULL THEN
+                EXIT;
+            END IF;
+        END LOOP;
+
+        IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
+            EXIT;
+        END IF;
+
+        SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
+
+    END LOOP;
+
+    IF max_fines.threshold IS NOT NULL THEN
+
+        FOR existing_sp_row IN
+                SELECT  *
+                  FROM  actor.usr_standing_penalty
+                  WHERE usr = match_user
+                        AND org_unit = max_fines.org_unit
+                        AND (stop_date IS NULL or stop_date > NOW())
+                        AND standing_penalty = 1
+                LOOP
+            RETURN NEXT existing_sp_row;
+        END LOOP;
+
+        SELECT  SUM(f.balance_owed) INTO current_fines
+          FROM  money.materialized_billable_xact_summary f
+                JOIN (
+                    SELECT  r.id
+                      FROM  booking.reservation r
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
+                    SELECT  g.id
+                      FROM  money.grocery g
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
+                    SELECT  circ.id
+                      FROM  action.circulation circ
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL ) l USING (id);
+
+        IF current_fines >= max_fines.threshold THEN
+            new_sp_row.usr := match_user;
+            new_sp_row.org_unit := max_fines.org_unit;
+            new_sp_row.standing_penalty := 1;
+            RETURN NEXT new_sp_row;
+        END IF;
+    END IF;
+
+    -- Start over for max overdue
+    SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
+
+    -- Fail if the user has too many overdue items
+    LOOP
+        tmp_grp := user_object.profile;
+        LOOP
+
+            SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id;
+
+            IF max_overdue.threshold IS NULL THEN
+                SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
+            ELSE
+                EXIT;
+            END IF;
+
+            IF tmp_grp IS NULL THEN
+                EXIT;
+            END IF;
+        END LOOP;
+
+        IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
+            EXIT;
+        END IF;
+
+        SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
+
+    END LOOP;
+
+    IF max_overdue.threshold IS NOT NULL THEN
+
+        FOR existing_sp_row IN
+                SELECT  *
+                  FROM  actor.usr_standing_penalty
+                  WHERE usr = match_user
+                        AND org_unit = max_overdue.org_unit
+                        AND (stop_date IS NULL or stop_date > NOW())
+                        AND standing_penalty = 2
+                LOOP
+            RETURN NEXT existing_sp_row;
+        END LOOP;
+
+        SELECT  INTO items_overdue COUNT(*)
+          FROM  action.circulation circ
+                JOIN  actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id)
+          WHERE circ.usr = match_user
+            AND circ.checkin_time IS NULL
+            AND circ.due_date < NOW()
+            AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL);
+
+        IF items_overdue >= max_overdue.threshold::INT THEN
+            new_sp_row.usr := match_user;
+            new_sp_row.org_unit := max_overdue.org_unit;
+            new_sp_row.standing_penalty := 2;
+            RETURN NEXT new_sp_row;
+        END IF;
+    END IF;
+
+    -- Start over for max out
+    SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
+
+    -- Fail if the user has too many checked out items
+    LOOP
+        tmp_grp := user_object.profile;
+        LOOP
+            SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id;
+
+            IF max_items_out.threshold IS NULL THEN
+                SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
+            ELSE
+                EXIT;
+            END IF;
+
+            IF tmp_grp IS NULL THEN
+                EXIT;
+            END IF;
+        END LOOP;
+
+        IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
+            EXIT;
+        END IF;
+
+        SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou;
+
+    END LOOP;
+
+
+    -- Fail if the user has too many items checked out
+    IF max_items_out.threshold IS NOT NULL THEN
+
+        FOR existing_sp_row IN
+                SELECT  *
+                  FROM  actor.usr_standing_penalty
+                  WHERE usr = match_user
+                        AND org_unit = max_items_out.org_unit
+                        AND (stop_date IS NULL or stop_date > NOW())
+                        AND standing_penalty = 3
+                LOOP
+            RETURN NEXT existing_sp_row;
+        END LOOP;
+
+        SELECT  INTO items_out COUNT(*)
+          FROM  action.circulation circ
+                JOIN  actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id)
+          WHERE circ.usr = match_user
+                AND circ.checkin_time IS NULL
+                AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL);
+
+           IF items_out >= max_items_out.threshold::INT THEN
+            new_sp_row.usr := match_user;
+            new_sp_row.org_unit := max_items_out.org_unit;
+            new_sp_row.standing_penalty := 3;
+            RETURN NEXT new_sp_row;
+           END IF;
+    END IF;
+
+    -- Start over for collections warning
+    SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org;
+
+    -- Fail if the user has a collections-level fine balance
+    LOOP
+        tmp_grp := user_object.profile;
+        LOOP
+            SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id;
+
+            IF max_fines.threshold IS NULL THEN
+                SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp;
+            ELSE
+                EXIT;
+            END IF;
+
+            IF tmp_grp IS NULL THEN
+                EXIT;
+            END IF;
+        END LOOP;
+
+        IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN
+            EXIT;
+        END IF;
+
+        SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou;
+
+    END LOOP;
+
+    IF max_fines.threshold IS NOT NULL THEN
+
+        FOR existing_sp_row IN
+                SELECT  *
+                  FROM  actor.usr_standing_penalty
+                  WHERE usr = match_user
+                        AND org_unit = max_fines.org_unit
+                        AND (stop_date IS NULL or stop_date > NOW())
+                        AND standing_penalty = 4
+                LOOP
+            RETURN NEXT existing_sp_row;
+        END LOOP;
+
+        SELECT  SUM(f.balance_owed) INTO current_fines
+          FROM  money.materialized_billable_xact_summary f
+                JOIN (
+                    SELECT  r.id
+                      FROM  booking.reservation r
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
+                    SELECT  g.id
+                      FROM  money.grocery g
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL
+                                UNION ALL
+                    SELECT  circ.id
+                      FROM  action.circulation circ
+                            JOIN  actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id)
+                      WHERE usr = match_user
+                            AND xact_finish IS NULL ) l USING (id);
+
+        IF current_fines >= max_fines.threshold THEN
+            new_sp_row.usr := match_user;
+            new_sp_row.org_unit := max_fines.org_unit;
+            new_sp_row.standing_penalty := 4;
+            RETURN NEXT new_sp_row;
+        END IF;
+    END IF;
+
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql b/Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql
new file mode 100644 (file)
index 0000000..337d5a7
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0106'); -- senator
+
+INSERT INTO permission.perm_list (id, code, description) VALUES
+    (352, 'ADMIN_BOOKING_RESOURCE', oils_i18n_gettext(352, 'Enables the user to create/update/delete booking resources', 'ppl', 'description')),
+    (353, 'ADMIN_BOOKING_RESOURCE_TYPE', oils_i18n_gettext(353, 'Enables the user to create/update/delete booking resource types', 'ppl', 'description')),
+    (354, 'ADMIN_BOOKING_RESOURCE_ATTR', oils_i18n_gettext(354, 'Enables the user to create/update/delete booking resource attributes', 'ppl', 'description')),
+    (355, 'ADMIN_BOOKING_RESOURCE_ATTR_MAP', oils_i18n_gettext(355, 'Enables the user to create/update/delete booking resource attribute maps', 'ppl', 'description')),
+    (356, 'ADMIN_BOOKING_RESOURCE_ATTR_VALUE', oils_i18n_gettext(356, 'Enables the user to create/update/delete booking resource attribute values', 'ppl', 'description')),
+    (357, 'ADMIN_BOOKING_RESERVATION', oils_i18n_gettext(357, 'Enables the user to create/update/delete booking reservations', 'ppl', 'description')),
+    (358, 'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP', oils_i18n_gettext(358, 'Enables the user to create/update/delete booking reservation attribute value maps', 'ppl', 'description'))
+;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql b/Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql
new file mode 100644 (file)
index 0000000..479f615
--- /dev/null
@@ -0,0 +1,20 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0109'); --miker
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype) VALUES (
+    'circ.booking_reservation.stop_circ',
+    'Disallow circulation of items when they are on booking reserve and that reserve overlaps with the checkout period',
+    'When true, items on booking reserve during the proposed checkout period will not be allowed to circulate unless overridden with the COPY_RESERVED.override permission.',
+    'bool'
+);
+
+INSERT INTO config.org_unit_setting_type (name, label, description, datatype) VALUES (
+    'circ.booking_reservation.default_elbow_room',
+    'Default amount of time by which a circulation should be shortened to allow for booking reservation delivery',
+    'When an item is on booking reserve, and that reservation overlaps with the proposed checkout period, and circulations have not been strictly disallowed on reserved items, Evergreen will attempt to adjust the due date of the circulation for this about of time before the beginning of the reservation period.  If this is not possible because the due date would end up in the past, the circulation is disallowed.',
+    'interval'
+);
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql b/Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql
new file mode 100644 (file)
index 0000000..69e98e5
--- /dev/null
@@ -0,0 +1,8 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0110'); --miker
+
+ALTER TABLE booking.resource_type ADD COLUMN elbow_room INTERVAL;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql b/Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql
new file mode 100644 (file)
index 0000000..e41334a
--- /dev/null
@@ -0,0 +1,7 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0122'); -- miker
+
+INSERT INTO config.copy_status (id,name) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name'));
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql b/Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql
new file mode 100644 (file)
index 0000000..b6bf9ae
--- /dev/null
@@ -0,0 +1,12 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0130'); -- senator
+
+ALTER TABLE booking.resource DROP CONSTRAINT br_unique;
+ALTER TABLE booking.resource ADD CONSTRAINT br_unique UNIQUE (owner, barcode);
+
+INSERT into permission.perm_list VALUES
+    (360, 'RETRIEVE_RESERVATION_PULL_LIST', oils_i18n_gettext(360, 'Allows a user to retrieve a booking reservation pull list', 'ppl', 'description')),
+    (361, 'CAPTURE_RESERVATION', oils_i18n_gettext(361, 'Allows a user to capture booking reservations', 'ppl', 'description')) ; 
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql b/Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql
new file mode 100644 (file)
index 0000000..c3a1814
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN;
+
+INSERT INTO config.upgrade_log (version) VALUES ('0119'); -- miker
+
+CREATE TABLE action.reservation_transit_copy (
+    reservation    INT REFERENCES booking.reservation (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
+) INHERITS (action.transit_copy);
+ALTER TABLE action.reservation_transit_copy ADD PRIMARY KEY (id);
+ALTER TABLE action.reservation_transit_copy ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy) REFERENCES booking.resource (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+CREATE INDEX active_reservation_transit_dest_idx ON "action".reservation_transit_copy (dest);
+CREATE INDEX active_reservation_transit_source_idx ON "action".reservation_transit_copy (source);
+CREATE INDEX active_reservation_transit_cp_idx ON "action".reservation_transit_copy (target_copy);
+
+COMMIT;
+
index 4dcf77d..5b3b516 100644 (file)
@@ -80,6 +80,7 @@ for my $d (@$user_data) {
        my $user                = $xact_data->{usr}->{__data__};
        my $circs       = $xact_data->{transactions}->{circulations};
        my $grocery = $xact_data->{transactions}->{grocery};
+       my $reservations = $xact_data->{transactions}->{reservations};
 
 
        # --------------------------------------------------------------------
@@ -96,7 +97,7 @@ for my $d (@$user_data) {
                        $a->{post_code}) . "\n";
        }
 
-       print_xact_details($_->{__data__}) for (@$circs, @$grocery);
+       print_xact_details($_->{__data__}) for (@$circs, @$grocery, @$reservations);
 
        print "\n" . '-'x60 . "\n";
 }
diff --git a/Open-ILS/web/css/skin/default/booking.css b/Open-ILS/web/css/skin/default/booking.css
new file mode 100644 (file)
index 0000000..2c42404
--- /dev/null
@@ -0,0 +1,89 @@
+div#brsrc_available_outer {
+    width: 50%;
+    float: left;
+    border-right: 1px solid #999999;
+}
+div#bra_and_brav {
+}
+div#reserve_right_side {
+    float: right;
+    width: 49%;
+    padding-left: 4px;
+}
+div#reserve_under {
+    clear: both;
+}
+div#reserve_datetime_start {
+    padding-bottom: 6px;
+}
+div#reserve_datetime_end {
+    padding-bottom: 6px;
+    border-bottom: 1px solid #999999;
+}
+label.bra {
+    font-style: italic;
+    padding-right: 12px;
+}
+h1.booking, h2.booking, h3.booking {
+    margin: 0;
+    padding-top: 0;
+    padding-bottom: 8px;
+}
+h1.booking { font-size: 16pt; font-weight: bold; }
+select#brsrc_list {
+    width: 90%;
+}
+label.reserve_datetime {
+    font-style: italic;
+    margin-bottom: 2px;
+}
+id#patron_barcode {
+    width: 150px;
+}
+div.nice_vertical_padding {
+    padding-top: 6px;
+    padding-bottom: 6px;
+}
+span.two_buttons {
+    text-align: center;
+}
+option.forced_unavailable {
+    background-color: #ffcccc;
+    color: #990000;
+    font-weight: bold;
+    font-style: italic;
+}
+input#arbitrary_resource { margin-left: 8px; margin-right: 8px; }
+div#or { font-size: 12pt; font-weight: bold; }
+input#interval_in_days { width: 75px; }
+table#the_table thead tr th {
+    vertical-align: top;
+    background-color: #dddddd;
+    color: #000000;
+    font-weight: bold;
+    padding: 0 6px 0 6px;
+    border-left: 1px #cccccc solid;
+    border-right: 1px #333333 solid;
+}
+tbody#the_table_body td {
+    vertical-align: top;
+    padding: 2px;
+    border-top: 1px #cccccc solid;
+    border-left: 1px #cccccc solid;
+    border-bottom: 1px #333333 solid;
+    border-right: 1px #333333 solid;
+}
+.capture_failure { color: #cc0000; }
+.capture_success { color: #00cc00; }
+ul { list-style-type: square; }
+.capture_info { font-size: 12pt; font-weight: bold; margin-bottom: 4px; }
+.transit_notice {
+    font-size: 12pt; font-weight: bold; color: #ff6666;
+    margin-bottom: 4px; margin-top: 4px;
+}
+span#result_display { margin-left: 12px; }
+div#contains_misc_controls { text-align:right; }
+div#patron_info { font-size: 12pt; font-weight: bold; }
+div#no_ready_bresv, div#no_out_bresv, div#no_in_bresv {
+    font-style: italic;
+}
diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/capture.js b/Open-ILS/web/js/dojo/openils/booking/nls/capture.js
new file mode 100644 (file)
index 0000000..7e78e11
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    'FAILURE': "Capture failed",
+    'SUCCESS': "Capture succeeded",
+    'UNKNOWN_PROBLEM': "An unknown problem occurred during capture attempt.",
+    'CAPTURED_NOTHING': "Didn't capture anything.",
+    'NO_PAYLOAD':
+        "We did not receive further information from the server about this" +
+        "attempt to capture.",
+    'HERES_WHAT_WE_KNOW':
+        "The following information is available about the failed capture:",
+    'CAPTURE_INFO': "Capture Information",
+    'CAPTURE_BRESV_DATES': "Reservation time:",
+    'CAPTURE_BRESV_BRSRC': "Resource barcode:",
+    'CAPTURE_BRESV_PICKUP_LIB': "Pickup library:",
+    'CAPTURE_BRESV_PATRON_BARCODE': "Patron barcode:",
+    'CAPTURE_CAUSES_TRANSIT': "This item is now in transit!",
+    'CAPTURE_TRANSIT_SOURCE': "From:",
+    'CAPTURE_TRANSIT_DEST': "To:",
+
+    'AUTO_capture_heading': "Capture Reserved Resources",
+    'AUTO_resource_barcode': "Enter barcode:",
+    'AUTO_pickup_lib_selector': "Pickup library:",
+    'AUTO_ATTR_VALUE_capture': "Capture"
+}
diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js b/Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js
new file mode 100644 (file)
index 0000000..6d786e3
--- /dev/null
@@ -0,0 +1,38 @@
+{
+    'NO_PATRON_BARCODE': "Please enter a patron barcode.",
+    'RESERVATIONS_NO_RESPONSE':
+        "No response from server when asking for reservations.",
+    'RESERVATIONS_ERROR':
+        "Error communicating with server (asking for reservations):",
+    'PICKUP_NO_RESPONSE': "No response from server when attempting pickup.",
+    'PICKUP_ERROR': "Error communicating with server (attempting pickup):",
+    'RETURN_NO_RESPONSE': "No response from server when attempting return.",
+    'RETURN_ERROR': "Error communicating with server (attempting return):",
+    'RETURN_SUCCESS': "Return successful.",
+    'SELECT_SOMETHING': "You have not selected any reservations.",
+    'NO_SUCH_RETURNABLE_RESOURCE': "No such returnable resource.",
+    'RETURNABLE_RESOURCE_ERROR': "Error looking up returnable resource:",
+    'NOTICE_CHANGE_OF_PATRON':
+        "Note that the resource scanned was out on reservation to different\n" +
+        "patron than the last resource you scanned.  If this is not\n" +
+        "expected, stop to examine outstanding reservations for your patron\n" +
+        "or on the resource.",
+
+    'AUTO_h1': "Reservations Pickup",
+    'AUTO_return_h1': "Reservations Return",
+    'AUTO_patron_barcode': "Enter patron barcode:",
+    'AUTO_barcode_type': "Return by barcode of",
+    'AUTO_in_bresv': "Patron has returned these resources today:",
+    'AUTO_ready_bresv': "Patron has these reservations ready for pickup:",
+    'AUTO_out_bresv': "Patron currently has these reservations out:",
+    'AUTO_no_ready_bresv':
+        "Patron has no reservations ready for pickup at this time.",
+    'AUTO_no_out_bresv': "Patron has no more reservations out at this time.",
+    'AUTO_no_in_bresv': "Patron has not returned any resources today.",
+    'AUTO_patron': "Patron",
+    'AUTO_resource': "Resource",
+    'AUTO_ATTR_VALUE_go': "Go",
+    'AUTO_ATTR_VALUE_reset': "Clear / New Patron",
+    'AUTO_ATTR_VALUE_pickup': "Pick up",
+    'AUTO_ATTR_VALUE_return': "Return"
+}
diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js b/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js
new file mode 100644 (file)
index 0000000..30ae8ec
--- /dev/null
@@ -0,0 +1,20 @@
+{
+    'PULL_LIST_NO_RESPONSE': "No response from server trying to get pull list!",
+    'PULL_LIST_ERROR': "Error trying to fetch pull list: ",
+    'COPY_LOOKUP_NO_RESPONSE': "No response looking up copies by barcode",
+    'COPY_LOOKUP_ERROR': "Error looking up copies by barcode: ",
+    'COPY_MISSING': "Unexpected error: No information for copy: ",
+
+    'AUTO_no_results': "No results",
+    'AUTO_owning_lib_selector': "See pull list for library:",
+    'AUTO_pull_list_title': "Booking Pull List",
+    'AUTO_interval_in_days': "Generate list for this many days hence: ",
+    'AUTO_ATTR_VALUE_fetch': "Fetch",
+    'AUTO_th_title_or_name': "Title or name",
+    'AUTO_th_barcode': "Barcode",
+    'AUTO_th_call_number': "Call number",
+    'AUTO_th_copy_location': "Copy location",
+    'AUTO_th_copy_number': "Copy number",
+    'AUTO_th_resv_details': "Reservation details",
+    'AUTO_ATTR_VALUE_print': "Print",
+}
diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js b/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js
new file mode 100644 (file)
index 0000000..76cf080
--- /dev/null
@@ -0,0 +1,82 @@
+{
+    'NO_BRT_RESULTS': "There are no bookable resource types registered.",
+    'NO_TARG_DIV': "Could not find target div",
+    'NO_BRA_RESULTS': "Couldn't retrieve booking resource attributes.",
+    'SELECT_A_BRSRC_THEN': "Select a resource from the big list above.",
+    'CREATE_BRESV_LOCAL_ERROR': "Exception trying to create reservation: ",
+    'CREATE_BRESV_SERVER_ERROR': "Server error trying to create reservation: ",
+    'CREATE_BRESV_SERVER_NO_RESPONSE':
+        "No response from server after trying to create reservation.",
+    /* FIXME: Users aren't likely to be able to do anything with the following
+     * message.  Figure out a way to do something more helpful.
+     */
+    'CREATE_BRESV_OK_MISSING_TARGET': function(n, m) {
+        return "Created " + n + " reservation(s), but " + m + " of these " +
+            "couldn't target any resources.\n\n" +
+            "This means that it won't be possible to fulfill some of these\n" +
+            "reservations until a suitable resource becomes available.";
+    },
+    'CREATE_BRESV_OK': function(n) {
+        return "Created " + n + " reservation" + (n == 1 ? "" : "s") + ".";
+    },
+    'WHERES_THE_BARCODE': "Enter a patron's barcode to make a reservation.",
+    'ACTOR_CARD_NOT_FOUND': "Patron barcode not found. Please try again.",
+    'GET_BRESV_LIST_ERR': "Error while retrieving reservation list: ",
+    'GET_BRESV_LIST_NO_RESULT':
+        "No results from server retrieving reservation list.",
+    'OUTSTANDING_BRESV': "Outstanding reservations for patron",
+    'UNTARGETED': "None targeted",
+    'GET_PATRON_NO_RESULT':
+        "No server response after attempting to look up patron by barcode.",
+    'HERE_ARE_EXISTING_BRESV': "Existing reservations for",
+    'NO_EXISTING_BRESV': "This user has no existing reservations at this time.",
+    'NO_USABLE_BRSRC':
+        "No reservable resources.  Adjust start and end time\n" +
+        "until a resource is available for reservation.",
+    'CXL_BRESV_SUCCESS': function(n) {
+        return ("Canceled " + n + " reservation" + (n == 1 ? "" : "s") + ".");
+    },
+    'CXL_BRESV_FAILURE': "Error canceling reservations; server silent.",
+    'CXL_BRESV_FAILURE2': "Error canceling reservations:\n",
+    'CXL_BRESV_SELECT_SOMETHING':
+        "You have not selected any reservations to cancel.",
+    'NEED_EXACTLY_ONE_BRT_PASSED_IN':
+        "Can't book multiple resource types at once",
+    'COULD_NOT_RETRIEVE_BRT_PASSED_IN':
+        "Error retrieving booking resource type",
+    'INVALID_TS_RANGE':
+        "You must choose a valid start and end time for the reservation.",
+    'BRSRC_NOT_FOUND': "Could not locate that resource.",
+    'BRSRC_RETRIVE_ERROR': "Error retrieving resource: ",
+    'ON_FLY_NO_RESPONSE':
+        "No response from server attempting to make item a bookable resource.",
+    'ON_FLY_ERROR':
+        "Error attempting to make item a bookable resource:",
+    'ANY': "ANY",
+
+    'AUTO_choose_a_brt': "Choose a Bookable Resource Type",
+    'AUTO_i_need_this_resource': "I need this resource...",
+    'AUTO_starting_at': "Between",
+    'AUTO_ending_at': "and",
+    'AUTO_with_these_attr': "With these attributes:",
+    'AUTO_patron_barcode': "Reserve to patron barcode:",
+    'AUTO_ATTR_VALUE_next': "Next",
+    'AUTO_ATTR_VALUE_reserve_brsrc': "Reserve Selected",
+    'AUTO_ATTR_VALUE_reserve_brt': "Reserve Any",
+    'AUTO_ATTR_VALUE_button_edit_existing': "Edit selected",
+    'AUTO_ATTR_VALUE_button_cancel_existing': "Cancel selcted",
+    'AUTO_bresv_grid_type': "Type",
+    'AUTO_bresv_grid_resource': "Resource",
+    'AUTO_bresv_grid_start_time': "Start time",
+    'AUTO_bresv_grid_end_time': "End time",
+    'AUTO_brt_noncat_only': "Show only non-cataloged bookable resource types",
+    'AUTO_arbitrary_resource':
+        "Enter the barcode of a cataloged, bookable resource:",
+    'AUTO_explain_bookable':
+        "To reserve an item that is not yet registered as a bookable " +
+        "resource, find it in the catalog or under <em>Display Item</em>, and "+
+        "select <em>Make Item Bookable</em> or <em>Book Item Now</em> there.",
+    'AUTO_pickup_lib_selector':
+        "Choose the pickup library for this reservation:",
+    'AUTO_or': '- Or -'
+}
diff --git a/Open-ILS/web/js/ui/default/booking/capture.js b/Open-ILS/web/js/ui/default/booking/capture.js
new file mode 100644 (file)
index 0000000..5502b5b
--- /dev/null
@@ -0,0 +1,174 @@
+dojo.require("openils.User");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+dojo.requireLocalization("openils.booking", "capture");
+
+const CAPTURE_FAILURE = 0;
+const CAPTURE_SUCCESS = 1;
+const CAPTURE_UNKNOWN = 2;
+
+var localeStrings = dojo.i18n.getLocalization("openils.booking", "capture");
+
+function CaptureDisplay(element) { this.element = element; }
+CaptureDisplay.prototype.no_payload = function() {
+    this.element.appendChild(document.createTextNode(localeStrings.NO_PAYLOAD));
+};
+CaptureDisplay.prototype.dump = function(payload) {
+    var div = document.createElement("div");
+    div.appendChild(document.createTextNode(localeStrings.HERES_WHAT_WE_KNOW));
+    this.element.appendChild(div);
+
+    var ul = document.createElement("ul");
+    for (var k in payload) {
+        var li = document.createElement("li");
+        li.appendChild(document.createTextNode(k + ": " + payload[k]));
+        ul.appendChild(li);
+    }
+    this.element.appendChild(ul);
+};
+CaptureDisplay.prototype.generate_transit_display = function(payload) {
+    var super_div = document.createElement("div");
+    var div;
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_CAUSES_TRANSIT
+    ));
+    div.setAttribute("class", "transit_notice");
+    super_div.appendChild(div);
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_TRANSIT_SOURCE + " " +
+        fieldmapper.aou.findOrgUnit(payload.transit.source()).shortname()
+    ));
+    super_div.appendChild(div);
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_TRANSIT_DEST + " " +
+        fieldmapper.aou.findOrgUnit(payload.transit.dest()).shortname()
+    ));
+    super_div.appendChild(div);
+
+    return super_div;
+};
+CaptureDisplay.prototype.display_with_transit_info = function(payload) {
+    var div;
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(localeStrings.CAPTURE_INFO));
+    div.setAttribute("class", "capture_info");
+    this.element.appendChild(div);
+
+    if (payload.catalog_item) {
+        div = document.createElement("div");
+        div.appendChild(document.createTextNode(
+            localeStrings.CAPTURE_BRESV_BRSRC + " " +
+            payload.catalog_item.barcode()
+        ));
+        this.element.appendChild(div);
+    }
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_BRESV_DATES + " " +
+        humanize_timestamp_string(payload.reservation.start_time()) + " - " +
+        humanize_timestamp_string(payload.reservation.end_time())
+    ));
+    this.element.appendChild(div);
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_BRESV_PICKUP_LIB + " " +
+        fieldmapper.aou.findOrgUnit(
+            payload.reservation.pickup_lib()
+        ).shortname()
+    ));
+    this.element.appendChild(div);
+
+    div = document.createElement("div");
+    div.appendChild(document.createTextNode(
+        localeStrings.CAPTURE_BRESV_PATRON_BARCODE + " " +
+        payload.reservation.usr().card().barcode()
+    ));
+    this.element.appendChild(div);
+
+    if (payload.transit) {
+        this.element.appendChild(this.generate_transit_display(payload));
+    }
+};
+CaptureDisplay.prototype.clear = function() { this.element.innerHTML = ""; };
+CaptureDisplay.prototype.load = function(payload) {
+    try {
+        this.element.appendChild(document.createElement("hr"));
+        if (!payload) {
+            this.no_payload();
+        } else if (!payload.fail_cause && payload.captured) {
+            this.display_with_transit_info(payload);
+        } else {
+            this.dump(payload);
+        }
+    } catch (E) {
+        alert(E); /* XXX */
+    }
+};
+
+var capture_display;
+var last_result;
+
+function clear_for_next() {
+    if (last_result == CAPTURE_SUCCESS) {
+        last_result = undefined;
+        document.getElementById("result_display").innerHTML = "";
+        document.getElementById("resource_barcode").value = "";
+    }
+}
+
+function capture() {
+    var barcode = document.getElementById("resource_barcode").value;
+    var result = fieldmapper.standardRequest(
+        [
+            "open-ils.booking",
+            "open-ils.booking.resources.capture_for_reservation"
+        ],
+        [xulG.auth.session.key, barcode]
+    );
+
+    if (result && result.ilsevent !== undefined) {
+        if (result.payload && result.payload.captured > 0) {
+            capture_display.load(result.payload);
+            return CAPTURE_SUCCESS;
+        } else {
+            capture_display.load(result.payload);
+            alert(my_ils_error(localeStrings.CAPTURED_NOTHING, result));
+            return CAPTURE_FAILURE;
+        }
+    } else {
+        return CAPTURE_UNKNOWN;
+    }
+}
+
+function attempt_capture() {
+    var rd = document.getElementById("result_display");
+    capture_display.clear();
+    switch(last_result = capture()) {
+        case CAPTURE_FAILURE:
+            rd.setAttribute("class", "capture_failure");
+            rd.innerHTML = localeStrings.FAILURE;
+            break;
+        case CAPTURE_SUCCESS:
+            rd.setAttribute("class", "capture_success");
+            rd.innerHTML = localeStrings.SUCCESS;
+            break;
+        default:
+            alert(localeStrings.UNKNOWN_PROBLEM);
+            break;
+    }
+}
+
+function my_init() {
+    init_auto_l10n(document.getElementById("auto_l10n_start_here"));
+    capture_display = new CaptureDisplay(
+        document.getElementById("capture_display")
+    );
+}
diff --git a/Open-ILS/web/js/ui/default/booking/common.js b/Open-ILS/web/js/ui/default/booking/common.js
new file mode 100644 (file)
index 0000000..cc6f302
--- /dev/null
@@ -0,0 +1,71 @@
+/* Quick and dirty way to localize some strings; not recommended for reuse.
+ * I'm sure dojo provides a better mechanism for this, but at the moment
+ * this is faster to implement anew than figuring out the Right way to do
+ * the same thing w/ dojo.
+ */
+function init_auto_l10n(el) {
+    function do_it(myel, cls) {
+        if (cls) {
+            var clss = cls.split(" ");
+            for (var k in clss) {
+                var parts = clss[k].match(/^AUTO_ATTR_([A-Z]+)_.+$/);
+                if (parts && localeStrings[clss[k]]) {
+                    myel.setAttribute(
+                        parts[1].toLowerCase(), localeStrings[clss[k]]
+                    );
+                } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) {
+                    myel.innerHTML = localeStrings[clss[k]];
+                }
+            }
+        }
+    }
+
+    for (var i in el.attributes) {
+        if (el.attributes[i].nodeName == "class") {
+            do_it(el, el.attributes[i].value);
+            break;
+        }
+    }
+    for (var i in el.childNodes) {
+        if (el.childNodes[i].nodeType == 1) { // element node?
+            init_auto_l10n(el.childNodes[i]); // recurse!
+        }
+    }
+}
+
+function get_keys(L) { var K = []; for (var k in L) K.push(k); return K; }
+function hide_dom_element(e) { e.style.display = "none"; };
+function reveal_dom_element(e) { e.style.display = ""; };
+function formal_name(u) {
+    var name = u.family_name() + ", " + u.first_given_name();
+    if (u.second_given_name())
+        name += (" " + u.second_given_name());
+    return name;
+}
+function humanize_timestamp_string(ts) {
+    /* For now, this discards time zones. */
+    var parts = ts.split("T");
+    var timeparts = parts[1].split("-")[0].split(":");
+    return parts[0] + " " + timeparts[0] + ":" + timeparts[1];
+}
+function is_ils_event(e) { return (e.ilsevent != undefined); }
+function is_ils_actor_card_error(e) {
+    return (e.textcode == "ACTOR_CARD_NOT_FOUND");
+}
+function my_ils_error(leader, e) {
+    var s = leader + "\n";
+    var keys = [
+        "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace"
+    ];
+    for (var i in keys) {
+        if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n");
+    }
+    return s;
+}
+function set_datagrid_empty_store(grid, flattener) {
+    grid.setStore(
+        new dojo.data.ItemFileReadStore(
+            {"data": flattener([])}
+        )
+    );
+}
diff --git a/Open-ILS/web/js/ui/default/booking/pickup.js b/Open-ILS/web/js/ui/default/booking/pickup.js
new file mode 100644 (file)
index 0000000..afa0527
--- /dev/null
@@ -0,0 +1,32 @@
+dojo.requireLocalization("openils.booking", "pickup_and_return");
+var localeStrings = dojo.i18n.getLocalization(
+    "openils.booking", "pickup_and_return"
+);
+var p;
+
+function react_to_pass_in(opts) {
+    if (opts && opts.patron_barcode) {
+        p.populate({"patron": opts.patron_barcode});
+
+        hide_dom_element(
+            document.getElementById("contains_barcode_control")
+        );
+        document.getElementById("patron_barcode").value = opts.patron_barcode;
+        p._extra_resetting = function() {
+            reveal_dom_element(
+                document.getElementById("contains_barcode_control")
+            );
+        };
+    }
+}
+
+function my_init() {
+    p = new Populator({
+        "ready": ready_bresv,
+        "out": out_bresv,
+        "patron": document.getElementById("patron_info")
+    }, document.getElementById("patron_barcode"));
+    init_auto_l10n(document.getElementById("auto_l10n_start_here"));
+
+    react_to_pass_in(xulG.bresv_interface_opts);
+}
diff --git a/Open-ILS/web/js/ui/default/booking/populator.js b/Open-ILS/web/js/ui/default/booking/populator.js
new file mode 100644 (file)
index 0000000..dc0d757
--- /dev/null
@@ -0,0 +1,283 @@
+/* This module depends on common.js being loaded, as well as the
+ * localization (Dojo/nls) for pickup and return . */
+
+dojo.require("dojo.data.ItemFileReadStore");
+
+function Populator(widgets, primary_input) {
+    this.widgets = widgets;
+
+    this.all = [];
+    for (var k in widgets) this.all.push(k);
+
+    if (primary_input) this.primary_input = primary_input;
+
+    this.prepare_cache();
+    this.prepare_empty_stores();
+    this.reset();
+}
+Populator.prototype.prepare_cache = function(data) {
+    this.cache = {};
+    for (var k in this.all) this.cache[this.all[k]] = {};
+};
+Populator.prototype.prepare_empty_stores = function(data) {
+    this.empty_stores = {};
+
+    for (var i in this.all) {
+        var name = this.all[i];
+
+        if (this.widgets[name] && this["flatten_" + name]) {
+            this.empty_stores[name] =
+                new dojo.data.ItemFileReadStore({
+                    "data": this["flatten_" + name]([])
+                });
+            this.widgets[name].setStore(this.empty_stores[name]);
+        }
+    }
+};
+Populator.prototype.flatten_ready = function(data) {
+    return {
+        "label": "id",
+        "identifier": "id",
+        "items": data.map(function(o) {
+            return {
+                "id": o.id(),
+                "type": o.target_resource_type().name(),
+                "resource": o.current_resource().barcode(),
+                "start_time": humanize_timestamp_string(o.start_time()),
+                "end_time": humanize_timestamp_string(o.end_time())
+            };
+        })
+    };
+};
+Populator.prototype.flatten_out = function(data) {
+    return {
+        "label": "id",
+        "identifier": "id",
+        "items": data.map(function(o) {
+            return {
+                "id": o.id(),
+                "type": o.target_resource_type().name(),
+                "resource": o.current_resource().barcode(),
+                "pickup_time": humanize_timestamp_string(o.pickup_time()),
+                "end_time": humanize_timestamp_string(o.end_time())
+            };
+        })
+    };
+};
+Populator.prototype.flatten_in = function(data) {
+    return {
+        "label": "id",
+        "identifier": "id",
+        "items": data.map(function(o) {
+            return {
+                "id": o.id(),
+                "type": o.target_resource_type().name(),
+                "resource": o.current_resource().barcode(),
+                "due_time": humanize_timestamp_string(o.end_time()),
+                "return_time": humanize_timestamp_string(o.return_time())
+            };
+        })
+    };
+};
+Populator.prototype.reveal_container = function(widget) {
+    var el = document.getElementById("contains_" + widget.id);
+    if (el) reveal_dom_element(el);
+};
+Populator.prototype.hide_container = function(widget) {
+    var el = document.getElementById("contains_" + widget.id);
+    if (el) hide_dom_element(el);
+};
+Populator.prototype.populate_ready = function(data) {
+    return this._populate_any_resv_grid(data, "ready");
+};
+Populator.prototype.populate_out = function(data) {
+    return this._populate_any_resv_grid(data, "out");
+};
+Populator.prototype.populate_in = function(data) {
+    return this._populate_any_resv_grid(data, "in");
+};
+Populator.prototype._populate_any_resv_grid = function(data, which) {
+    var flattener = this["flatten_" + which];
+    var widget = this.widgets[which];
+    var cache = this.cache[which];
+    var empty_store = this.empty_stores[which];
+
+    this.reveal_container(widget);
+
+    if (!data || !data.length) {
+        widget.setStore(empty_store);
+        this.toggle_anyness(false, which);
+    } else {
+        for (var i in data) cache[data[i].id()] = data[i];
+
+        widget.setStore(
+            new dojo.data.ItemFileReadStore({"data": flattener(data)})
+        );
+
+        this.toggle_anyness(true, which);
+
+        /* Arrrgh! Horrid but necessary: */
+        setTimeout(function() { widget.sort(); }, 100);
+    }
+};
+Populator.prototype.populate_patron = function(data) {
+    var h2 = document.createElement("h2");
+    h2.setAttribute("class", "booking");
+    h2.appendChild(document.createTextNode(formal_name(data)));
+
+    this.widgets.patron.innerHTML = "";
+    this.widgets.patron.appendChild(h2);
+
+    this.reveal_container(this.widgets.patron);
+    /* Maybe add patron's home OU or something here later... */
+};
+Populator.prototype.return_by_resource = function(barcode) {
+    /* XXX instead of talking to the server every time we do this, we could
+     * also check the "out" cache, iff we have one.  */
+    var r = fieldmapper.standardRequest(
+        ["open-ils.booking",
+        "open-ils.booking.reservations.by_returnable_resource_barcode"],
+        [xulG.auth.session.key, barcode]
+    );
+    if (!r || r.length < 1) {
+        alert(localeStrings.NO_SUCH_RETURNABLE_RESOURCE);
+    } else if (is_ils_event(r)) {
+        alert(my_ils_error(localeStrings.RETURNABLE_RESOURCE_ERROR, r));
+    } else {
+        try {
+            var new_barcode = r.usr().card().barcode();
+        } catch (E) {
+            alert(localeStrings.RETURN_ERROR + "\nr: " + js2JSON(r) + "\n" + E);
+            return;
+        }
+        if (this.patron_barcode && this.patron_barcode != new_barcode) {
+            /* XXX make this more subtle, i.e. flash something in background */
+            alert(localeStrings.NOTICE_CHANGE_OF_PATRON);
+        }
+        this.patron_barcode = new_barcode;
+        var ret = this.return(r);
+        if (!ret) {
+            alert(localeStrings.RETURN_NO_RESPONSE);
+        } else if (is_ils_event(ret) && ret.textcode != "SUCCESS") {
+            alert(my_ils_error(localeStrings.RETURN_ERROR, ret));
+        } else {
+            /* XXX speedbump should go, but something has to happen else
+             * there's no indication to staff that anything happened when
+             * starting from a fresh (blank) return interface.
+             */
+            alert(localeStrings.RETURN_SUCCESS);
+        }
+        this.populate(); /* Won't recurse with no args. All is well. */
+    }
+};
+Populator.prototype.populate = function(barcode, which) {
+    if (barcode) {
+        if (barcode.patron) {
+            this.patron_barcode = barcode.patron;
+        }
+        else if (barcode.resource) { /* resource OR patron, not both */
+            if (!this.return_by_resource(barcode.resource))
+                return;
+        }
+    }
+    if (!this.patron_barcode) {
+        alert(localeStrings.NO_PATRON_BARCODE);
+        return;
+    }
+
+    if (!which) which = this.all;
+
+    var result = fieldmapper.standardRequest(
+        ["open-ils.booking", "open-ils.booking.reservations.get_captured"],
+        [xulG.auth.session.key, this.patron_barcode, which]
+    );
+
+    if (!result) {
+        this.patron_barcode = undefined;
+        alert(localeStrings.RESERVATIONS_NO_RESPONSE);
+    } else if (is_ils_event(result)) {
+        this.patron_barcode = undefined;
+        alert(my_ils_error(localeStrings.RESERVATIONS_ERROR, result));
+    } else {
+        for (var k in result)
+            this["populate_" + k](result[k]);
+    }
+};
+Populator.prototype.toggle_anyness = function(any, which) {
+    var widget = this.widgets[which].domNode;
+    var empty_alternate = document.getElementById("no_" + widget.id);
+    var controls = document.getElementById("controls_" + widget.id);
+    if (any) {
+        reveal_dom_element(widget);
+        if (empty_alternate) hide_dom_element(empty_alternate);
+        if (controls) reveal_dom_element(controls);
+    } else {
+        hide_dom_element(widget);
+        if (empty_alternate) reveal_dom_element(empty_alternate);
+        if (controls) hide_dom_element(controls);
+    }
+};
+Populator.prototype.pickup = function(reservation) {
+    return fieldmapper.standardRequest(
+        ["open-ils.circ", "open-ils.circ.reservation.pickup"],
+        [xulG.auth.session.key, {
+            "patron_barcode": this.patron_barcode,
+            "reservation": reservation
+        }]
+    );
+};
+Populator.prototype.return = function(reservation) {
+    return fieldmapper.standardRequest(
+        ["open-ils.circ", "open-ils.circ.reservation.return"],
+        [xulG.auth.session.key, {
+            "patron_barcode": this.patron_barcode,
+            "reservation": reservation.id()
+            /* yeah just id here ------^; lack of parallelism */
+        }]
+    );
+};
+Populator.prototype.act_on_selected = function(how, which) {
+    var widget = this.widgets[which];
+    var cache = this.cache[which];
+    var no_response_msg = localeStrings[how.toUpperCase() + "_NO_RESPONSE"];
+    var error_msg = localeStrings[how.toUpperCase() + "_ERROR"];
+
+    var selected_id_list =
+        widget.selection.getSelected().map(function(o) { return o.id[0]; });
+
+    if (!selected_id_list || !selected_id_list.length) {
+        alert(localeStrings.SELECT_SOMETHING);
+        return;
+    }
+
+    var reservations = selected_id_list.map(function(o) { return cache[o]; });
+
+    /* Do we have to process these one at a time?  I think so... */
+    for (var i in reservations) {
+        var result = this[how](reservations[i]);
+        if (!result) {
+            alert(no_response_msg);
+        } else if (is_ils_event(result) && result.textcode != "SUCCESS") {
+            alert(my_ils_error(error_msg, result));
+        } else {
+            continue;
+        }
+        break;
+    }
+
+    this.populate();
+};
+Populator.prototype.reset = function() {
+    for (var k in this.widgets) {
+        this.hide_container(this.widgets[k]);
+    }
+    this.patron_barcode = undefined;
+
+    if (typeof(this._extra_resetting) == "function")
+        this._extra_resetting();
+
+    if (this.primary_input) {
+        this.primary_input.value = "";
+        this.primary_input.focus();
+    }
+};
diff --git a/Open-ILS/web/js/ui/default/booking/pull_list.js b/Open-ILS/web/js/ui/default/booking/pull_list.js
new file mode 100644 (file)
index 0000000..996cc42
--- /dev/null
@@ -0,0 +1,203 @@
+dojo.require("openils.User");
+dojo.require("openils.PermaCrud");
+dojo.require("fieldmapper.OrgUtils");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+dojo.requireLocalization("openils.booking", "pull_list");
+
+var localeStrings = dojo.i18n.getLocalization("openils.booking", "pull_list");
+var pcrud = new openils.PermaCrud();
+
+var owning_lib_selected;
+var acp_cache = {};
+
+function init_owning_lib_selector() {
+    var User = new openils.User();
+    User.buildPermOrgSelector(
+        "RETRIEVE_RESERVATION_PULL_LIST", owning_lib_selector, null,
+        function() {
+            owning_lib_selected = owning_lib_selector.getValue();
+            dojo.connect(owning_lib_selector, "onChange",
+                function() { owning_lib_selected = this.getValue(); }
+            )
+        }
+    );
+}
+
+function retrieve_pull_list(ivl_in_days) {
+    var secs = Number(ivl_in_days) * 86400;
+
+    if (isNaN(secs) || secs < 1)
+        throw new Error("Invalid interval");
+
+    return fieldmapper.standardRequest(
+        ["open-ils.booking", "open-ils.booking.reservations.get_pull_list"],
+        [xulG.auth.session.key, null, secs, owning_lib_selected]
+    );
+}
+
+function dom_table_rowid(resource_id) {
+    return "pull_list_resource_" + resource_id;
+}
+
+function generate_result_row(one) {
+    function cell(id, content) {
+        var td = document.createElement("td");
+        if (id != undefined) td.setAttribute("id", id);
+        td.appendChild(document.createTextNode(content));
+        return td;
+    }
+
+    function reservation_info_cell(one) {
+        var td = document.createElement("td");
+        for (var i in one.reservations) {
+            var one_resv = one.reservations[i];
+            var div = document.createElement("div");
+            var s = humanize_timestamp_string(one_resv.start_time()) + " - " +
+                humanize_timestamp_string(one_resv.end_time()) + " " +
+                formal_name(one_resv.usr());
+            /* FIXME: The above need patron barcode instead of name, but
+             * that requires a fix in the middle layer to flesh on the
+             * right stuff. */
+            div.appendChild(document.createTextNode(s));
+            td.appendChild(div);
+        }
+        return td;
+    }
+
+    var baseid = dom_table_rowid(one.current_resource.id());
+
+    var cells = [];
+    cells.push(cell(undefined, one.target_resource_type.name()));
+    cells.push(cell(undefined, one.current_resource.barcode()));
+    cells.push(cell(baseid + "_call_number", "-"));
+    cells.push(cell(baseid + "_copy_location", "-"));
+    cells.push(cell(baseid + "_copy_number", "-"));
+    cells.push(reservation_info_cell(one));
+
+    var row = document.createElement("tr");
+    row.setAttribute("id", baseid);
+
+    for (var i in cells) row.appendChild(cells[i]);
+    return row;
+}
+
+function render_pull_list_fundamentals(list) {
+    var rows = [];
+
+    for (var i in list)
+        rows.push(generate_result_row(list[i]));
+
+    document.getElementById("the_table_body").innerHTML = "";
+
+    for (var i in rows)
+        document.getElementById("the_table_body").appendChild(rows[i]);
+}
+
+function get_all_relevant_acp(list) {
+    var barcodes = [];
+    for (var i in list) {
+        if (list[i].target_resource_type.catalog_item()) {
+            /* There shouldn't be any duplicates. No need to worry bout that */
+            barcodes.push(list[i].current_resource.barcode());
+        }
+    }
+    if (barcodes.length > 0) {
+        var results = fieldmapper.standardRequest(
+            [
+                "open-ils.booking",
+                "open-ils.booking.asset.get_copy_fleshed_just_right"
+            ],
+            [xulG.auth.session.key, barcodes]
+        );
+
+        if (!results) {
+            alert(localeStrings.COPY_LOOKUP_NO_RESPONSE);
+            return null;
+        } else if (is_ils_event(results)) {
+            alert(my_ils_error(localeStrings.COPY_LOOKUP_ERROR, results));
+            return null;
+        } else {
+            return results;
+        }
+    }
+}
+
+function fill_in_pull_list_details(list, acp_cache) {
+    for (var i in list) {
+        var one = list[i];
+        if (one.target_resource_type.catalog_item() == "t") {
+            /* FIXME: This block could stand to be a lot more elegant. */
+            var call_number_el = document.getElementById(
+                dom_table_rowid(one.current_resource.id()) + "_call_number"
+            );
+            var copy_location_el = document.getElementById(
+                dom_table_rowid(one.current_resource.id()) + "_copy_location"
+            );
+            var copy_number_el = document.getElementById(
+                dom_table_rowid(one.current_resource.id()) + "_copy_number"
+            );
+
+            var bc = one.current_resource.barcode();
+
+            if (acp_cache[bc]) {
+                if (call_number_el && acp_cache[bc].call_number()) {
+                    var value = acp_cache[bc].call_number().label();
+                    if (value) call_number_el.innerHTML = value;
+                }
+                if (copy_location_el && acp_cache[bc].location()) {
+                    var value = acp_cache[bc].location().name();
+                    if (value) copy_location_el.innerHTML = value;
+                }
+                if (copy_number_el) {
+                    var value = acp_cache[bc].copy_number();
+                    if (value) copy_number_el.innerHTML = value;
+                }
+            } else {
+                alert(localeStrings.COPY_MISSING + bc);
+            }
+        }
+    }
+}
+
+function populate_pull_list(form) {
+    /* Step 1: get the pull list from the server. */
+    try {
+        var results = retrieve_pull_list(form.interval_in_days.value);
+    } catch (E) {
+        alert(localeStrings.PULL_LIST_ERROR + E);
+        return;
+    }
+    if (results == null) {
+        alert(localeStrings.PULL_LIST_NO_RESPONSE);
+        return;
+    } else if (is_ils_event(results)) {
+        alert(my_ils_error(localeStrings.PULL_LIST_ERROR, results));
+        return;
+    }
+
+    if (results.length) {
+        reveal_dom_element(document.getElementById("table_goes_here"));
+        hide_dom_element(document.getElementById("no_results"));
+
+        /* Step 2: render the table with the pull list */
+        render_pull_list_fundamentals(results);
+
+        /* Step 3: asynchronously fill in the copy details we're missing */
+        setTimeout(function() {
+            var acp_cache = {};
+            if ((acp_cache = get_all_relevant_acp(results)))
+                fill_in_pull_list_details(results, acp_cache);
+        }, 0);
+    } else {
+        hide_dom_element(document.getElementById("table_goes_here"));
+        reveal_dom_element(document.getElementById("no_results"));
+    }
+
+}
+
+function my_init() {
+    hide_dom_element(document.getElementById("table_goes_here"));
+    hide_dom_element(document.getElementById("no_results"));
+    init_owning_lib_selector();
+    init_auto_l10n(document.getElementById("auto_l10n_start_here"));
+}
diff --git a/Open-ILS/web/js/ui/default/booking/reservation.js b/Open-ILS/web/js/ui/default/booking/reservation.js
new file mode 100644 (file)
index 0000000..8e97b22
--- /dev/null
@@ -0,0 +1,768 @@
+/*
+ * Details, details...
+ */
+dojo.require("fieldmapper.OrgUtils");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+dojo.require("dojo.data.ItemFileReadStore");
+dojo.require("dijit.form.DateTextBox");
+dojo.require("dijit.form.TimeTextBox");
+dojo.requireLocalization("openils.booking", "reservation");
+
+/*
+ * Globals; prototypes and their instances
+ */
+var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation");
+var pcrud = new openils.PermaCrud();
+var opts;
+var our_brt;
+var pickup_lib_selected;
+var brt_list = [];
+var brsrc_index = {};
+var bresv_index = {};
+var just_reserved_now = {};
+
+function AttrValueTable() { this.t = {}; }
+AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; };
+AttrValueTable.prototype.update_from_selector = function(selector) {
+    var attr  = selector.name.match(/_(\d+)$/)[1];
+    var value = selector.options[selector.selectedIndex].value;
+    if (attr)
+        attr_value_table.set(attr, value);
+};
+AttrValueTable.prototype.get_all_values = function() {
+    var values = [];
+    for (var k in this.t) {
+        if (this.t[k] != undefined && this.t[k] != "")
+            values.push(this.t[k]);
+    }
+    return values;
+};
+var attr_value_table =  new AttrValueTable();
+
+function TimestampRange() {
+    this.start = new Date();
+    this.end = new Date();
+
+    this.validity = {"start": false, "end": false};
+    this.nodes = {
+        "start": {"date": undefined, "time": undefined},
+        "end": {"date": undefined, "time": undefined}
+    };
+    this.saved_style_properties = {};
+    this.invalid_style_properties = {
+        "backgroundColor": "#ffcccc",
+        "color": "#990000",
+        "borderColor": "#990000",
+        "fontWeight": "bold"
+    };
+}
+TimestampRange.prototype.get_timestamp = function(when) {
+    return this.any_widget.serialize(this[when]).
+        replace("T", " ").substr(0, 19);
+};
+TimestampRange.prototype.get_range = function() {
+    return this.is_backwards() ?
+        [this.get_timestamp("end"), this.get_timestamp("start")] :
+        [this.get_timestamp("start"), this.get_timestamp("end")];
+};
+TimestampRange.prototype.update_from_widget = function(widget) {
+    var when = widget.id.match(/(start|end)/)[1];
+    var which = widget.id.match(/(date|time)/)[1];
+
+    if (this.any_widget == undefined)
+        this.any_widget = widget;
+    if (this.nodes[when][which] == undefined)
+        this.nodes[when][which] = widget.domNode; /* We'll need this later */
+
+    if (when && which) {
+        this.update_timestamp(when, which, widget.value);
+    }
+
+    this.compute_validity();
+    this.paint_validity();
+};
+TimestampRange.prototype.compute_validity = function() {
+    if (Math.abs(this.start - this.end) < 1000) {
+        this.validity.end = false;
+    } else {
+        if (this.start < this.current_minimum())
+            this.validity.start = false;
+        else
+            this.validity.start = true;
+
+        if (this.end < this.current_minimum())
+            this.validity.end = false;
+        else
+            this.validity.end = true;
+    }
+};
+/* This method provides the minimum timestamp that is considered valid. For
+ * now it's arbitrarily "now + 15 minutes", meaning that all reservations
+ * must be made at least 15 minutes in the future.
+ *
+ * For reasons of keeping the middle layer happy, this should always return
+ * a time that is at least somewhat in the future. The ML isn't able to target
+ * any resources for a reservation with a start date that isn't in the future.
+ */
+TimestampRange.prototype.current_minimum = function() {
+    /* XXX This is going to be a problem with local clocks that are off. */
+    var n = new Date();
+    n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */
+    return n;
+};
+TimestampRange.prototype.update_timestamp = function(when, which, value) {
+    if (which == "date") {
+        this[when].setFullYear(value.getFullYear());
+        this[when].setMonth(value.getMonth());
+        this[when].setDate(value.getDate());
+    } else {    /* "time" */
+        this[when].setHours(value.getHours());
+        this[when].setMinutes(value.getMinutes());
+        this[when].setSeconds(0);
+    }
+};
+TimestampRange.prototype.is_backwards = function() {
+    return (this.start > this.end);
+};
+TimestampRange.prototype.paint_validity = function()  {
+    for (var when in this.validity) {
+        if (this.validity[when]) {
+            this.paint_valid_node(this.nodes[when].date);
+            this.paint_valid_node(this.nodes[when].time);
+        } else {
+            this.paint_invalid_node(this.nodes[when].date);
+            this.paint_invalid_node(this.nodes[when].time);
+        }
+    }
+};
+TimestampRange.prototype.paint_invalid_node = function(node) {
+    if (node) {
+        /* Just toggling the class of something would be better than
+         * manually setting style here, but I haven't been able to get that
+         * to play nicely with dojo's styling of the date/time textboxen.
+         */
+        if (this.saved_style_properties.backgroundColor == undefined) {
+            for (var k in this.invalid_style_properties) {
+                this.saved_style_properties[k] = node.style[k];
+            }
+        }
+        for (var k in this.invalid_style_properties) {
+            node.style[k] = this.invalid_style_properties[k];
+        }
+    }
+};
+TimestampRange.prototype.paint_valid_node = function(node) {
+    if (node) {
+        for (var k in this.saved_style_properties) {
+            node.style[k] = this.saved_style_properties[k];
+        }
+    }
+};
+TimestampRange.prototype.is_valid = function() {
+    return (this.validity.start && this.validity.end);
+};
+var reserve_timestamp_range = new TimestampRange();
+
+function SelectorMemory(selector) {
+    this.selector = selector;
+    this.memory = {};
+}
+SelectorMemory.prototype.save = function() {
+    for (var i = 0; i < this.selector.options.length; i++) {
+        if (this.selector.options[i].selected) {
+            this.memory[this.selector.options[i].value] = true;
+        }
+    }
+};
+SelectorMemory.prototype.restore = function() {
+    for (var i = 0; i < this.selector.options.length; i++) {
+        if (this.memory[this.selector.options[i].value]) {
+            if (!this.selector.options[i].disabled)
+                this.selector.options[i].selected = true;
+        }
+    }
+};
+
+/*
+ * These functions communicate with the middle layer.
+ */
+function get_all_noncat_brt() {
+    return pcrud.search("brt",
+        {"id": {"!=": null}, "catalog_item": "f"},
+        {"order_by": {"brt":"name"}}
+    );
+}
+
+function get_brt_by_id(id) {
+    return pcrud.retrieve("brt", id);
+}
+
+function get_brsrc_id_list() {
+    var options = {"type": our_brt.id(), "pickup_lib": pickup_lib_selected};
+
+    /* This mechanism for avoiding the passing of an empty 'attribute_values'
+     * option is essential because if you pass such an option to the
+     * middle layer API at all, it won't return any IDs for brsrcs that
+     * don't have at least one attribute of some kind.
+     */
+    var attribute_values = attr_value_table.get_all_values();
+    if (attribute_values.length > 0)
+        options.attribute_values = attribute_values;
+
+    options.available = reserve_timestamp_range.get_range();
+
+    return fieldmapper.standardRequest(
+        ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"],
+        [xulG.auth.session.key, options]
+    );
+}
+
+/* FIXME: We need failure checking after pcrud.retrieve() */
+function add_brsrc_to_index_if_needed(list, further) {
+    for (var i in list) {
+        if (!brsrc_index[list[i]]) {
+            brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]);
+        }
+        if (further)
+            further(brsrc_index[list[i]]);
+    }
+}
+
+function sync_brsrc_index_from_ids(available_list, additional_list) {
+    /* Default states for everything in the index. Read the further comments. */
+    for (var i in brsrc_index) {
+        brsrc_index[i].isdeleted(true);
+        brsrc_index[i].ischanged(false);
+    }
+
+    /* Populate the cache with anything that's missing and tag everything
+     * in the "available" list as *not* deleted, and tag everything in the
+     * additional list as "changed." See below. */
+    add_brsrc_to_index_if_needed(
+        available_list, function(o) { o.isdeleted(false); }
+    );
+    add_brsrc_to_index_if_needed(
+        additional_list,
+        function(o) {
+            if (!(o.id() in just_reserved_now)) o.ischanged(true);
+        }
+    );
+    /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper
+     * attributes of the brsrcs in our cache.  Because we're not going to
+     * pass back any brsrcs to the middle layer, it doesn't really matter
+     * what we set this attribute to. What we're using it for is to indicate
+     * in our little brsrc cache how a given brsrc should be displayed in this
+     * UI's current state (based on whether the brsrc matches timestamp range
+     * availability (isdeleted(false)) and whether the brsrc has been forced
+     * into the list because it was selected in a previous interface (like
+     * the catalog) (ischanged(true))).
+     */
+}
+
+function check_bresv_targeting(results) {
+    var missing = 0;
+    for (var i in results) {
+        if (!(results[i].targeting && results[i].targeting.current_resource)) {
+            missing++;
+        } else {
+            just_reserved_now[results[i].targeting.current_resource] = true;
+        }
+    }
+    return missing;
+}
+
+function create_bresv(resource_list) {
+    var barcode = document.getElementById("patron_barcode").value;
+    if (barcode == "") {
+        alert(localeStrings.WHERES_THE_BARCODE);
+        return;
+    } else if (!reserve_timestamp_range.is_valid()) {
+        alert(localeStrings.INVALID_TS_RANGE);
+        return;
+    }
+    var results;
+    try {
+        results = fieldmapper.standardRequest(
+            ["open-ils.booking", "open-ils.booking.reservations.create"],
+            [
+                xulG.auth.session.key,
+                barcode,
+                reserve_timestamp_range.get_range(),
+                pickup_lib_selected,
+                our_brt.id(),
+                resource_list,
+                attr_value_table.get_all_values()
+            ]
+        );
+    } catch (E) {
+        alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E);
+    }
+    if (results) {
+        if (is_ils_event(results)) {
+            if (is_ils_actor_card_error(results)) {
+                alert(localeStrings.ACTOR_CARD_NOT_FOUND);
+            } else {
+                alert(my_ils_error(
+                    localeStrings.CREATE_BRESV_SERVER_ERROR, results
+                ));
+            }
+        } else {
+            var missing;
+            alert((missing = check_bresv_targeting(results)) ?
+                localeStrings.CREATE_BRESV_OK_MISSING_TARGET(
+                    results.length, missing
+                ) :
+                localeStrings.CREATE_BRESV_OK(results.length)
+            );
+            update_brsrc_list();
+            update_bresv_grid();
+        }
+    } else {
+        alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE);
+    }
+}
+
+function flatten_to_dojo_data(obj_list) {
+    return {
+        "label": "id",
+        "identifier": "id",
+        "items": obj_list.map(function(o) {
+            var new_obj = {
+                "id": o.id(),
+                "type": o.target_resource_type().name(),
+                "start_time": humanize_timestamp_string(o.start_time()),
+                "end_time": humanize_timestamp_string(o.end_time()),
+            };
+
+            if (o.current_resource())
+                new_obj["resource"] = o.current_resource().barcode();
+            else if (o.target_resource())
+                new_obj["resource"] = "* " + o.target_resource().barcode();
+            else
+                new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *";
+            return new_obj;
+        })
+    };
+}
+
+function create_bresv_on_brsrc() {
+    var selector = document.getElementById("brsrc_list");
+    var selected_values = [];
+    for (var i in selector.options) {
+        if (selector.options[i] && selector.options[i].selected)
+            selected_values.push(selector.options[i].value);
+    }
+    if (selected_values.length > 0)
+        create_bresv(selected_values);
+    else
+        alert(localeStrings.SELECT_A_BRSRC_THEN);
+}
+
+function create_bresv_on_brt() {
+    if (any_usable_brsrc())
+        create_bresv();
+    else
+        alert(localeStrings.NO_USABLE_BRSRC);
+}
+
+function get_actor_by_barcode(barcode) {
+    var usr = fieldmapper.standardRequest(
+        ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"],
+        [xulG.auth.session.key, barcode]
+    );
+    if (usr == null) {
+        alert(localeStrings.GET_PATRON_NO_RESULT);
+    } else if (is_ils_event(usr)) {
+        return null; /* XXX inelegant: this function is quiet about errors
+                        here because to report them would be redundant with
+                        another function that gets called right after this one.
+                      */
+    } else {
+        return usr;
+    }
+}
+
+function init_bresv_grid(barcode) {
+    var result = fieldmapper.standardRequest(
+        ["open-ils.booking",
+            "open-ils.booking.reservations.filtered_id_list"
+        ],
+        [xulG.auth.session.key, {
+            "user_barcode": barcode,
+            "fields": {
+                "pickup_time": null,
+                "cancel_time": null,
+                "return_time": null
+            }
+        }, /* whole_obj */ true]
+    );
+    if (result == null) {
+        set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
+        alert(localeStrings.GET_BRESV_LIST_NO_RESULT);
+    } else if (is_ils_event(result)) {
+        set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data);
+        if (is_ils_actor_card_error(result)) {
+            alert(localeStrings.ACTOR_CARD_NOT_FOUND);
+        } else {
+            alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result));
+        }
+    } else {
+        if (result.length < 1) {
+            document.getElementById("bresv_grid_alt_explanation").innerHTML =
+                localeStrings.NO_EXISTING_BRESV;
+            hide_dom_element(document.getElementById("bresv_grid"));
+            reveal_dom_element(document.getElementById("reserve_under"));
+        } else {
+            document.getElementById("bresv_grid_alt_explanation").innerHTML =
+                "";
+            reveal_dom_element(document.getElementById("bresv_grid"));
+            reveal_dom_element(document.getElementById("reserve_under"));
+        }
+        /* May as well do the following in either case... */
+        bresvGrid.setStore(
+            new dojo.data.ItemFileReadStore(
+                {"data": flatten_to_dojo_data(result)}
+            )
+        );
+        bresv_index = {};
+        for (var i in result) {
+            bresv_index[result[i].id()] = result[i];
+        }
+    }
+}
+
+function cancel_reservations(bresv_id_list) {
+    try {
+        var result = fieldmapper.standardRequest(
+            ["open-ils.booking", "open-ils.booking.reservations.cancel"],
+            [xulG.auth.session.key, bresv_id_list]
+        );
+    } catch (E) {
+        alert(localeStrings.CXL_BRESV_FAILURE2 + E);
+        return;
+    }
+    setTimeout(update_bresv_grid, 0);
+    if (!result) {
+        alert(localeStrings.CXL_BRESV_FAILURE);
+    } else if (is_ils_event(result)) {
+        alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result));
+    } else {
+        alert(localeStrings.CXL_BRESV_SUCCESS(result.length));
+    }
+}
+
+function munge_specific_resource(barcode) {
+    try {
+        var copy_list = pcrud.search(
+            "acp", {"barcode": barcode, "deleted": "f"}
+        );
+        if (copy_list && copy_list.length > 0) {
+            var r = fieldmapper.standardRequest(
+                ["open-ils.booking",
+                    "open-ils.booking.resources.create_from_copies"],
+                [xulG.auth.session.key,
+                    copy_list.map(function(o) { return o.id(); })]
+            );
+
+            if (!r) {
+                alert(localeStrings.ON_FLY_NO_RESPONSE);
+            } else if (is_ils_event(r)) {
+                alert(my_ils_error(localeStrings.ON_FLY_ERROR, r));
+            } else {
+                if (!(our_brt = get_brt_by_id(r.brt[0][0]))) {
+                    alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
+                } else {
+                    opts.booking_results = r;
+                    init_reservation_interface();
+                }
+            }
+        } else {
+            alert(localeStrings.BRSRC_NOT_FOUND);
+        }
+    } catch (E) {
+        alert(localeStrings.BRSRC_RETRIEVE_ERROR + E);
+    }
+}
+
+/*
+ * These functions deal with interface tricks (populating widgets,
+ * changing the page, etc.).
+ */
+function init_pickup_lib_selector() {
+    var User = new openils.User();
+    User.buildPermOrgSelector(
+        "ADMIN_BOOKING_RESERVATION", pickup_lib_selector, null,
+        function() {
+            pickup_lib_selected = pickup_lib_selector.getValue();
+            dojo.connect(pickup_lib_selector, "onChange",
+                function() {
+                    pickup_lib_selected = this.getValue();
+                    update_brsrc_list();
+                }
+            )
+        }
+    );
+}
+
+function provide_brt_selector(targ_div) {
+    if (!targ_div) {
+        alert(localeStrings.NO_TARG_DIV);
+    } else {
+        brt_list = get_all_noncat_brt();
+        if (!brt_list || brt_list.length < 1) {
+            document.getElementById("select_noncat_brt_block").
+                style.display = "none";
+        } else {
+            var selector = document.createElement("select");
+            selector.setAttribute("id", "brt_selector");
+            selector.setAttribute("name", "brt_selector");
+            /* I'm reluctantly hardcoding this "size" attribute as 8
+             * because you can't accomplish this with CSS anyway.
+             */
+            selector.setAttribute("size", 8);
+            for (var i in brt_list) {
+                var option = document.createElement("option");
+                option.setAttribute("value", brt_list[i].id());
+                option.appendChild(document.createTextNode(brt_list[i].name()));
+                selector.appendChild(option);
+            }
+            targ_div.innerHTML = "";
+            targ_div.appendChild(selector);
+        }
+    }
+}
+
+function init_resv_iface_arb() {
+    init_reservation_interface(document.getElementById("arbitrary_resource"));
+}
+
+function init_resv_iface_sel() {
+    init_reservation_interface(document.getElementById("brt_selector"));
+}
+
+function init_reservation_interface(widget) {
+    /* Save a global reference to the brt we're going to reserve */
+    if (widget && (widget.selectedIndex != undefined)) {
+        our_brt = brt_list[widget.selectedIndex];
+    } else if (widget != undefined) {
+        if (!munge_specific_resource(widget.value))
+            return;
+    }
+
+    /* Hide and reveal relevant divs. */
+    var search_block = document.getElementById("brt_search_block");
+    var reserve_block = document.getElementById("brt_reserve_block");
+    hide_dom_element(search_block);
+    reveal_dom_element(reserve_block);
+
+    /* Get a list of attributes that can apply to that brt. */
+    var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()});
+    if (!bra_list) {
+        alert(localeString.NO_BRA_LIST);
+        return;
+    }
+
+    /* Get a table of values that can apply to the above attributes. */
+    var brav_by_bra = {};
+    bra_list.map(function(o) {
+        brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()});
+    });
+
+    /* Hide the label over the attributes widgets if we have nothing to show. */
+    var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element;
+    domf(document.getElementById("bra_and_brav_header"));
+
+    /* Create DOM widgets to represent each attribute/values set. */
+    for (var i in bra_list) {
+        var bra_div = document.createElement("div");
+        bra_div.setAttribute("class", "nice_vertical_padding");
+
+        var bra_select = document.createElement("select");
+        bra_select.setAttribute("name", "bra_" + bra_list[i].id());
+        bra_select.setAttribute(
+            "onchange",
+            "attr_value_table.update_from_selector(this); update_brsrc_list();"
+        );
+
+        var bra_opt_any = document.createElement("option");
+        bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY));
+        bra_opt_any.setAttribute("value", "");
+
+        bra_select.appendChild(bra_opt_any);
+
+        var bra_label = document.createElement("label");
+        bra_label.setAttribute("class", "bra");
+        bra_label.appendChild(document.createTextNode(bra_list[i].name()));
+
+        var j = bra_list[i].id();
+        for (var k in brav_by_bra[j]) {
+            var bra_opt = document.createElement("option");
+            bra_opt.setAttribute("value", brav_by_bra[j][k].id());
+            bra_opt.appendChild(
+                document.createTextNode(brav_by_bra[j][k].valid_value())
+            );
+            bra_select.appendChild(bra_opt);
+        }
+
+        bra_div.appendChild(bra_label);
+        bra_div.appendChild(bra_select);
+        document.getElementById("bra_and_brav").appendChild(bra_div);
+    }
+    /* Add a prominent label reminding the user what resource type they're
+     * asking about. */
+    document.getElementById("brsrc_list_header").innerHTML = our_brt.name();
+    init_pickup_lib_selector();
+    update_brsrc_list();
+}
+
+function update_brsrc_list() {
+    var brsrc_id_list = get_brsrc_id_list();
+    var force_list = (opts.booking_results && opts.booking_results.brsrc) ?
+        opts.booking_results.brsrc.map(function(o) { return o[0]; }) : [];
+
+    sync_brsrc_index_from_ids(brsrc_id_list, force_list);
+
+    var target_selector = document.getElementById("brsrc_list");
+    var selector_memory = new SelectorMemory(target_selector);
+    selector_memory.save();
+    target_selector.innerHTML = "";
+
+    for (var i in brsrc_index) {
+        if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged()))
+            continue;
+
+        var opt = document.createElement("option");
+        opt.setAttribute("value", brsrc_index[i].id());
+        opt.appendChild(document.createTextNode(brsrc_index[i].barcode()));
+
+        if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) {
+            opt.setAttribute("class", "forced_unavailable");
+            opt.setAttribute("disabled", "disabled");
+        }
+
+        target_selector.appendChild(opt);
+    }
+
+    selector_memory.restore();
+}
+
+function any_usable_brsrc() {
+    for (var i in brsrc_index) {
+        if (!brsrc_index[i].isdeleted())
+            return true;
+    }
+    return false;
+}
+
+function update_bresv_grid() {
+    var widg = document.getElementById("patron_barcode");
+    if (widg.value != "") {
+        setTimeout(function() {
+            var target = document.getElementById(
+                "existing_reservation_patron_line"
+            );
+            var patron = get_actor_by_barcode(widg.value);
+            if (patron) {
+                target.innerHTML = (
+                    localeStrings.HERE_ARE_EXISTING_BRESV + " " +
+                    formal_name(patron) + ": "
+                );
+            } else {
+                target.innerHTML = "";
+            }
+        }, 0);
+        setTimeout(function() { init_bresv_grid(widg.value); }, 0);
+    }
+}
+
+function init_timestamp_widgets() {
+    var when = ["start", "end"];
+    for (var i in when) {
+        reserve_timestamp_range.update_from_widget(
+            new dijit.form.TimeTextBox({
+                name: "reserve_time_" + when[i],
+                value: new Date(),
+                constraints: {
+                    timePattern: "HH:mm",
+                    clickableIncrement: "T00:15:00",
+                    visibleIncrement: "T00:15:00",
+                    visibleRange: "T01:30:00",
+                },
+                onChange: function() {
+                    reserve_timestamp_range.update_from_widget(this);
+                    update_brsrc_list();
+                }
+            }, "reserve_time_" + when[i])
+        );
+        reserve_timestamp_range.update_from_widget(
+            new dijit.form.DateTextBox({
+                name: "reserve_date_" + when[i],
+                value: new Date(),
+                onChange: function() {
+                    reserve_timestamp_range.update_from_widget(this);
+                    update_brsrc_list();
+                }
+            }, "reserve_date_" + when[i])
+        );
+    }
+}
+
+function cancel_selected_bresv(bresv_dojo_items) {
+    if (bresv_dojo_items && bresv_dojo_items.length > 0 &&
+        (bresv_dojo_items[0].length == undefined ||
+            bresv_dojo_items[0].length > 0)) {
+        cancel_reservations(
+            bresv_dojo_items.map(function(o) { return o.id[0]; })
+        );
+        /* After some delay to allow the cancellations a chance to get
+         * committed, refresh the brsrc list as it might reflect newly
+         * available resources now. */
+        setTimeout(update_brsrc_list, 2000);
+    } else {
+        alert(localeStrings.CXL_BRESV_SELECT_SOMETHING);
+    }
+}
+
+/* The following function should return true if the reservation interface
+ * should start normally (show a list of brt to choose from) or false if
+ * it should not (because we've "started" it some other way by setting up
+ * and displaying other widgets).
+ */
+function early_action_passthru() {
+    if (opts.booking_results) {
+        if (opts.booking_results.brt.length != 1) {
+            alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN);
+            return true;
+        } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) {
+            alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN);
+            return true;
+        }
+
+        init_reservation_interface();
+        return false;
+    }
+
+    if (opts.patron_barcode) {
+        document.getElementById("contain_patron_barcode").style.display="none";
+        document.getElementById("patron_barcode").value = opts.patron_barcode;
+        update_bresv_grid();
+    }
+
+    return true;
+}
+
+/*
+ * my_init
+ */
+function my_init() {
+    hide_dom_element(document.getElementById("brt_reserve_block"));
+    reveal_dom_element(document.getElementById("brt_search_block"));
+    hide_dom_element(document.getElementById("reserve_under"));
+    init_auto_l10n(document.getElementById("auto_l10n_start_here"));
+    init_timestamp_widgets();
+
+    if (!(opts = xulG.bresv_interface_opts)) opts = {};
+    if (early_action_passthru())
+        provide_brt_selector(document.getElementById("brt_selector_here"));
+}
diff --git a/Open-ILS/web/js/ui/default/booking/return.js b/Open-ILS/web/js/ui/default/booking/return.js
new file mode 100644 (file)
index 0000000..64c9cf8
--- /dev/null
@@ -0,0 +1,41 @@
+dojo.requireLocalization("openils.booking", "pickup_and_return");
+var localeStrings = dojo.i18n.getLocalization(
+    "openils.booking", "pickup_and_return"
+);
+var p;
+
+function react_to_pass_in(opts) {
+    if (opts && opts.patron_barcode) {
+        p.populate({"patron": opts.patron_barcode});
+
+        hide_dom_element(
+            document.getElementById("contains_barcode_control")
+        );
+
+        document.getElementById("barcode").value = opts.patron_barcode;
+        var barcode_type = document.getElementById("barcode_type");
+        for (var i in barcode_type.options) {
+            if (barcode_type.options[i].value == "patron") {
+                barcode_type.selectedIndex = i;
+                break;
+            }
+        }
+
+        p._extra_resetting = function() {
+            reveal_dom_element(
+                document.getElementById("contains_barcode_control")
+            );
+        };
+    }
+}
+
+function my_init() {
+    p = new Populator({
+        "out": out_bresv,
+        "in": in_bresv,
+        "patron": document.getElementById("patron_info")
+    }, document.getElementById("barcode"));
+    init_auto_l10n(document.getElementById("auto_l10n_start_here"));
+
+    react_to_pass_in(xulG.bresv_interface_opts);
+}
index d5eeee3..05bbbac 100644 (file)
 <!ENTITY staff.cat.opac.delete_record.label "Delete Record">
 <!ENTITY staff.cat.opac.undelete_record.accesskey "U">
 <!ENTITY staff.cat.opac.undelete_record.label "Undelete Record">
+<!ENTITY staff.cat.opac.create_brt_from_record.accesskey "T">
+<!ENTITY staff.cat.opac.create_brt_from_record.label "Make Item Bookable">
 <!ENTITY staff.cat.opac.menu.accesskey "A">
 <!ENTITY staff.cat.opac.menu.label "Actions for this Record">
 <!ENTITY staff.cat.opac.opac_view.accesskey "O">
 <!ENTITY staff.main.menu.admin.server_admin.conify.z3950_source.label "Z39.50 Servers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.circulation_modifier.label "Circulation Modifiers">
 
+<!ENTITY staff.main.menu.admin.server_admin.booking.label "Booking">
+<!ENTITY staff.main.menu.admin.server_admin.booking.accesskey "B">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource.label "Resources">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource.accesskey "R">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_type.label "Resource Types">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_type.accesskey "T">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr.label "Resource Attributes">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr.accesskey "A">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr_value.label "Resource Attribute Values">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr_value.accesskey "V">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr_map.label "Resource Attribute Maps">
+<!ENTITY staff.main.menu.admin.server_admin.booking.resource_attr_map.accesskey "M">
 
 <!ENTITY staff.main.menu.admin.developer.label "For developers...">
 <!ENTITY staff.main.menu.admin.download_patrons.accesskey "D">
 <!ENTITY staff.main.menu.acq.upload.label "Load Order Record">
 <!ENTITY staff.main.menu.acq.po.label "Purchase Orders">
 
+<!ENTITY staff.main.menu.booking.label "Booking">
+<!ENTITY staff.main.menu.booking.accesskey "B">
+<!ENTITY staff.main.menu.booking.reservation.label "Create or Edit Reservations">
+<!ENTITY staff.main.menu.booking.reservation.accesskey "C">
+<!ENTITY staff.main.menu.booking.pull_list.label "Pull List">
+<!ENTITY staff.main.menu.booking.pull_list.accesskey "L">
+<!ENTITY staff.main.menu.booking.capture.label "Capture Resources">
+<!ENTITY staff.main.menu.booking.capture.accesskey "A">
+<!ENTITY staff.main.menu.booking.reservation_pickup.label "Pick Up Reservations">
+<!ENTITY staff.main.menu.booking.reservation_pickup.accesskey "P">
+<!ENTITY staff.main.menu.booking.reservation_return.label "Return Reservations">
+<!ENTITY staff.main.menu.booking.reservation_return.accesskey "R">
+
 <!ENTITY staff.main.menu.acq.fund.label "Funds">
 <!ENTITY staff.main.menu.acq.funding_source.label "Funding Sources">
 <!ENTITY staff.main.menu.acq.provider.label "Providers">
 <!ENTITY staff.patron_navbar.holds.accesskey 'H'>
 <!ENTITY staff.patron_navbar.alert 'Display Alert and Messages'>
 <!ENTITY staff.patron_navbar.alert.accesskey 'A'>
+<!ENTITY staff.patron_navbar.booking 'Booking'>
+<!ENTITY staff.patron_navbar.booking.accesskey 'k'>
 <!ENTITY staff.patron_navbar.other 'Other'>
 <!ENTITY staff.patron_navbar.other.accesskey 'o'>
 <!ENTITY staff.patron_navbar.items 'Items Out'>
 <!ENTITY staff.server.admin.index.transit_list "Transit List">
 <!ENTITY staff.server.admin.index.conify "Server Settings">
 
+<!ENTITY staff.server.admin.index.booking "Booking">
+<!ENTITY staff.server.admin.index.booking.reservation "Create/Cancel Reservations">
+<!ENTITY staff.server.admin.index.booking.pull_list "Pull List">
+<!ENTITY staff.server.admin.index.booking.capture "Capture">
+<!ENTITY staff.server.admin.index.booking.pickup "Pickup Reservations">
+<!ENTITY staff.server.admin.index.booking.return "Return Reservations">
+
 
 <!ENTITY staff.server.admin.org_settings.title "Evergreen: Library Settings Editor">
 <!-- This will be followed by the user's name -->
 <!ENTITY staff.circ.copy_status_overlay.sel_copy_details.accesskey "I">
 <!ENTITY staff.circ.copy_status_overlay.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.circ.copy_status_overlay.sel_patron.accesskey "L">
+<!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.label "Book Item Now">
+<!ENTITY staff.circ.copy_status_overlay.cmd_book_item_now.accesskey "N">
+<!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.label "Make Item Bookable">
+<!ENTITY staff.circ.copy_status_overlay.cmd_create_brt.accesskey "K">
 <!ENTITY staff.circ.copy_status_overlay.sel_edit.label "Edit Item Attributes">
 <!ENTITY staff.circ.copy_status_overlay.sel_edit.accesskey "E">
 <!ENTITY staff.circ.copy_status_overlay.sel_mark_items_damaged.label "Mark Item Damaged">
 <!ENTITY staff.cat.copy_browser.actions.cmd_add_items_to_buckets.accesskey "B">
 <!ENTITY staff.cat.copy_browser.actions.sel_copy_details.label "Show Item Details">
 <!ENTITY staff.cat.copy_browser.actions.sel_copy_details.accesskey "I">
+<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.label "Book Item Now">
+<!ENTITY staff.cat.copy_browser.actions.cmd_book_item_now.accesskey "N">
+<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.label "Make Item Bookable">
+<!ENTITY staff.cat.copy_browser.actions.cmd_create_brt.accesskey "K">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.cat.copy_browser.actions.sel_patron.accesskey "L">
 <!ENTITY staff.cat.copy_browser.actions.cmd_edit_items.label "Edit Item Attributes">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.add_items_to_bucket.accesskey "B">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.sel_copy_details.label "Show Item Details">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.sel_copy_details.accesskey "I">
+<!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_create_brt.label "Make This Item Bookable">
+<!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_create_brt.accesskey "Y">
+
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.sel_patron.label "Show Last Few Circulations">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.sel_patron.accesskey "L">
 <!ENTITY staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.label "Edit Item Attributes">
index 2be360c..8876fc4 100644 (file)
@@ -616,6 +616,12 @@ function myOPACShowTransactions(r) {
 
                else if(trans.xact_type() == 'grocery' ) 
                        myopacShowGenericTransaction( trans );
+
+/*      XXX need to copy circulation output function here
+               else if(trans.xact_type() == 'reservation' ) 
+                       myopacShowReservationTransaction( trans );
+*/
+
        }
 }
 
diff --git a/Open-ILS/web/templates/default/booking/capture.tt2 b/Open-ILS/web/templates/default/booking/capture.tt2
new file mode 100644 (file)
index 0000000..d7baf87
--- /dev/null
@@ -0,0 +1,21 @@
+[% WRAPPER "default/base.tt2" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/common.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/capture.js"></script>
+<link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/booking.css" />
+<script type="text/javascript">openils.Util.addOnLoad(my_init);</script>
+<div id="auto_l10n_start_here">
+<!-- XXX This interface will probably go away soon in favor of merging its
+behavior into the regular checkin/process/capture interface. -->
+    <h1 class="AUTO_capture_heading booking"></h1>
+    <form class="nice_vertical_padding"
+        onsubmit="attempt_capture(); return false">
+        <label for="resource_barcode" class="AUTO_resource_barcode"></label>
+        <input id="resource_barcode" onfocus="clear_for_next();" />
+        <input type="button" class="AUTO_ATTR_VALUE_capture"
+            onclick="attempt_capture();" />
+        <span id="result_display"></span>
+    </form>
+    <div class="nice_vertical_padding" id="capture_display">
+    </div>
+</div>
+[% END %]
diff --git a/Open-ILS/web/templates/default/booking/pickup.tt2 b/Open-ILS/web/templates/default/booking/pickup.tt2
new file mode 100644 (file)
index 0000000..396e9eb
--- /dev/null
@@ -0,0 +1,77 @@
+[% WRAPPER "default/base.tt2" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/common.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/populator.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/pickup.js"></script>
+<link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/booking.css" />
+<script type="text/javascript">
+    dojo.require("dojox.grid.DataGrid");
+    openils.Util.addOnLoad(my_init);
+
+    function act(f) {
+        p.populate({"patron": f.patron_barcode.value});
+        return false; /* Always. */
+    }
+</script>
+<div id="auto_l10n_start_here">
+    <h1 class="booking AUTO_h1"></h1>
+    <div class="nice_vertical_padding" id="contains_barcode_control">
+        <form id="lookup" onsubmit="return act(this);">
+            <label for="patron_barcode" class="AUTO_patron_barcode"></label>
+            <input id="patron_barcode" name="patron_barcode" />
+            <input type="submit" class="AUTO_ATTR_VALUE_go" />
+        </form>
+    </div>
+    <div class="nice_vertical_padding" id="contains_patron_info">
+        <div id="patron_info"></div>
+    </div>
+    <div class="nice_vertical_padding" id="contains_ready_bresv">
+        <h3 class="booking AUTO_ready_bresv"></h3>
+        <div class="AUTO_no_ready_bresv" id="no_ready_bresv"></div>
+        <table id="ready_bresv" jsId="ready_bresv"
+            dojoType="dojox.grid.DataGrid" query="{id: '*'}"
+            rowSelector="20px" autoHeight="true" width="auto">
+            <thead>
+                <tr><!-- FIXME: i18n problem: init_auto_l10n() runs
+                        too late to take care of the below elements. -->
+                    <th width="35%" field="type">Title</th>
+                    <th width="25%" field="resource">Barcode</th>
+                    <th width="20%" field="start_time">Start time</th>
+                    <th width="20%" field="end_time">End time</th>
+                </tr>
+            </thead>
+        </table>
+        <div class="nice_vertical_padding" id="controls_ready_bresv">
+            <form>
+                <input type="button" id="pickup_button"
+                    class="AUTO_ATTR_VALUE_pickup"
+                    onclick="p.act_on_selected('pickup', 'ready');" />
+            </form>
+        </div>
+    </div>
+    <div class="nice_vertical_padding" id="contains_out_bresv">
+        <hr />
+        <h3 class="booking AUTO_out_bresv"></h3>
+        <div class="AUTO_no_out_bresv" id="no_out_bresv"></div>
+        <table id="out_bresv" jsId="out_bresv"
+            dojoType="dojox.grid.DataGrid" query="{id: '*'}"
+            rowSelector="20px" autoHeight="true" width="auto">
+            <thead>
+                <tr><!-- FIXME: i18n problem: init_auto_l10n() runs
+                        too late to take care of the below elements. -->
+                    <th width="35%" field="type">Title</th>
+                    <th width="25%" field="resource">Barcode</th>
+                    <th width="20%" field="pickup_time">Pickup time</th>
+                    <th width="20%" field="end_time">Due time</th>
+                </tr>
+            </thead>
+        </table>
+    </div>
+    <div class="nice_vertical_padding" id="contains_misc_controls">
+        <hr />
+        <form>
+            <input type="button" class="AUTO_ATTR_VALUE_reset"
+                onclick="p.reset();" />
+        </form>
+    </div>
+</div>
+[% END %]
diff --git a/Open-ILS/web/templates/default/booking/pull_list.tt2 b/Open-ILS/web/templates/default/booking/pull_list.tt2
new file mode 100644 (file)
index 0000000..90de206
--- /dev/null
@@ -0,0 +1,50 @@
+[% WRAPPER "default/base.tt2" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/common.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/pull_list.js"></script>
+<link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/booking.css" />
+<script type="text/javascript">openils.Util.addOnLoad(my_init);</script>
+<div id="auto_l10n_start_here">
+    <h1 class="booking AUTO_pull_list_title"></h1>
+    <form onsubmit="populate_pull_list(this); return false;">
+        <div id="owning_lib_selector_row" class="nice_vertical_padding">
+            <label for="owning_lib_selector" class="AUTO_owning_lib_selector">
+            </label>
+            <select dojoType="openils.widget.OrgUnitFilteringSelect"
+                id="owning_lib_selector" jsId="owning_lib_selector"
+                searchAttr="shortname" labelAttr="shortname"></select>
+        </div>
+        <div id="interval_input_row" class="nice_vertical_padding">
+            <label for="interval_in_days" class="AUTO_interval_in_days"></label>
+
+            <!-- XXX Hardcoded values (like the ones below) are bad. -->
+            <input id="interval_in_days" name="interval_in_days"
+                value="5" maxlength="2" />
+
+        </div>
+        <input type="submit" class="AUTO_ATTR_VALUE_fetch" />
+    </form>
+    <hr />
+    <div id="table_goes_here" class="nice_vertical_padding">
+        <table id="the_table" width="100%">
+            <thead>
+                <tr>
+                    <th width="30%" class="AUTO_th_title_or_name"></th>
+                    <th width="10%" class="AUTO_th_barcode"></th>
+                    <th width="10%" class="AUTO_th_call_number"></th>
+                    <th width="10%" class="AUTO_th_copy_location"></th>
+                    <th width="10%" class="AUTO_th_copy_number"></th>
+                    <th width="30%" class="AUTO_th_resv_details"></th>
+                </tr>
+            </thead>
+            <tbody id="the_table_body">
+            </tbody>
+        </table>
+        <div id="print_holder" class="nice_vertical_padding">
+            <!-- XXX Print button probably won't stay right here -->
+            <input type="button" class="AUTO_ATTR_VALUE_print"
+                onclick="window.print();" /><!-- XXX too simplistic? -->
+        </div>
+    </div>
+    <div id="no_results" class="AUTO_no_results"></div>
+</div>
+[% END %]
diff --git a/Open-ILS/web/templates/default/booking/reservation.tt2 b/Open-ILS/web/templates/default/booking/reservation.tt2
new file mode 100644 (file)
index 0000000..b21d11e
--- /dev/null
@@ -0,0 +1,117 @@
+[% WRAPPER "default/base.tt2" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/common.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/reservation.js"></script>
+<link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/booking.css" />
+<script type="text/javascript">
+    dojo.require("dojox.grid.DataGrid");
+    openils.Util.addOnLoad(my_init);
+</script>
+<div id="auto_l10n_start_here">
+    <div id="brt_search_block" class="container">
+        <h1 class="booking AUTO_choose_a_brt"></h1>
+        <form onsubmit="return false;">
+            <div id="select_noncat_brt_block">
+                <div id="brt_selector_here" class="nice_vertical_padding"></div>
+                <div class="nice_vertical_padding">
+                    <input type="button" class="AUTO_ATTR_VALUE_next"
+                        onclick="init_resv_iface_sel(); return false"
+                        />
+                </div>
+                <hr />
+                <div class="nice_vertical_padding AUTO_or" id="or"></div>
+            </div>
+            <div id="arbitrary_resource_block">
+                <label for="arbitrary_resource" class="AUTO_arbitrary_resource">
+                </label>
+                <input id="arbitrary_resource" name="arbitrary_resource" />
+                <input type="button"
+                    onclick="init_resv_iface_arb(); return false;"
+                    class="AUTO_ATTR_VALUE_next" />
+                <p class="AUTO_explain_bookable"></p>
+            </div>
+        </form>
+    </div>
+
+    <div id="brt_reserve_block" class="container">
+        <form onsubmit="return false;">
+            <div id="brsrc_available_outer">
+                <h1 class="booking" id="brsrc_list_header"></h1>
+                <!-- I'm reluctantly hardcoding the size attribute below to 12
+                    since you can't get the behavior of the size attribute with
+                    anything in CSS. -->
+                <select id="brsrc_list" name="brsrc_list" multiple="multiple"
+                    size="12"></select>
+                <div id="contain_patron_barcode" class="nice_vertical_padding">
+                    <label class="AUTO_patron_barcode"
+                        for="patron_barcode" /></label>
+                    <input name="patron_barcode" id="patron_barcode"
+                        onchange="update_bresv_grid();" />
+                </div>
+                <div id="pickup_lib_selector_row" class="nice_vertical_padding">
+                    <label for="pickup_lib_selector"
+                        class="AUTO_pickup_lib_selector"></label>
+                    <select dojoType="openils.widget.OrgUnitFilteringSelect"
+                        id="pickup_lib_selector" jsId="pickup_lib_selector"
+                        searchAttr="shortname" labelAttr="shortname"></select>
+                </div>
+                <div class="nice_vertical_padding">
+                    <span class="two_buttons">
+                        <input type="button"
+                            class="AUTO_ATTR_VALUE_reserve_brsrc"
+                            onclick="create_bresv_on_brsrc();" />
+                        &nbsp;
+                        <input type="button"
+                            class="AUTO_ATTR_VALUE_reserve_brt"
+                            onclick="create_bresv_on_brt();" />
+                    </span>
+                </div>
+            </div>
+            <div id="reserve_right_side">
+                <h2 class="booking AUTO_i_need_this_resource"></h2>
+                <div id="reserve_datetime_start">
+                    <label class="reserve_datetime AUTO_starting_at"
+                        for="reserve_date_start"></label><br />
+                    <input id="reserve_date_start" />
+                    <input id="reserve_time_start" />
+                </div>
+                <div id="reserve_datetime_end">
+                    <label class="reserve_datetime AUTO_ending_at"
+                        for="reserve_date_end"></label><br />
+                    <input id="reserve_date_end" />
+                    <input id="reserve_time_end" />
+                </div>
+                <h2 id="bra_and_brav_header"
+                    class="booking AUTO_with_these_attr"></h2>
+                <div id="bra_and_brav"></div>
+            </div>
+        </form>
+    </div>
+
+    <div id="reserve_under">
+        <hr />
+        <h2 class="booking" id="existing_reservation_patron_line"></h2>
+        <div id="bresv_grid_alt_explanation"></div>
+        <table id="bresv_grid" jsId="bresvGrid"
+            dojoType="dojox.grid.DataGrid" query="{id: '*'}"
+            rowSelector="20px" autoHeight="true" width="auto">
+            <thead>
+                <tr><!-- FIXME: i18n problem: init_auto_l10n() runs
+                        too late to take care of the below elements. -->
+                    <th width="35%" field="type">Type</th>
+                    <th width="25%" field="resource">Resource</th>
+                    <th width="20%" field="start_time">Start time</th>
+                    <th width="20%" field="end_time">End time</th>
+                </tr>
+            </thead>
+        </table>
+        <div class="nice_vertical_padding"
+            id="existing_bresv_under_buttons">
+            <!-- <input type="button" id="button_edit_existing"
+                class="AUTO_ATTR_VALUE_button_edit_existing" /> -->
+            <input type="button" id="button_cancel_existing"
+                class="AUTO_ATTR_VALUE_button_cancel_existing"
+                onclick="cancel_selected_bresv(bresvGrid.selection.getSelected());" />
+        </div>
+    </div>
+</div>
+[% END %]
diff --git a/Open-ILS/web/templates/default/booking/return.tt2 b/Open-ILS/web/templates/default/booking/return.tt2
new file mode 100644 (file)
index 0000000..47bc853
--- /dev/null
@@ -0,0 +1,87 @@
+[% WRAPPER "default/base.tt2" %]
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/common.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/populator.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/booking/return.js"></script>
+<link rel="stylesheet" type="text/css" href="[% ctx.media_prefix %]/css/skin/[% ctx.skin %]/booking.css" />
+<script type="text/javascript">
+    dojo.require("dojox.grid.DataGrid");
+    openils.Util.addOnLoad(my_init);
+
+    function act(f) {
+        var key = f.barcode_type.options[f.barcode_type.selectedIndex].value;
+        var obj = {};
+        obj[key] = f.barcode.value;
+        p.populate(obj);
+        return false; /* Always. */
+    }
+</script>
+<div id="auto_l10n_start_here">
+    <h1 class="booking AUTO_return_h1"></h1>
+    <div class="nice_vertical_padding" id="contains_barcode_control">
+        <form id="lookup" onsubmit="return act(this);">
+            <label for="barcode_type" class="AUTO_barcode_type"></label>
+            <select name="barcode_type" id="barcode_type"
+                onchange="var b = this.form.barcode; b.focus(); b.select();">
+                <option id="option_resource" value="resource"
+                    selected="selected" class="AUTO_resource"></option>
+                <option id="option_patron" value="patron"
+                    class="AUTO_patron"></option>
+            </select>
+            <input id="barcode" name="barcode" />
+            <input type="submit" class="AUTO_ATTR_VALUE_go" />
+        </form>
+    </div>
+    <div class="nice_vertical_padding" id="contains_patron_info">
+        <div id="patron_info"></div>
+    </div>
+    <div class="nice_vertical_padding" id="contains_out_bresv">
+        <h3 class="booking AUTO_out_bresv"></h3>
+        <div class="AUTO_no_out_bresv" id="no_out_bresv"></div>
+        <table id="out_bresv" jsId="out_bresv"
+            dojoType="dojox.grid.DataGrid" query="{id: '*'}"
+            rowSelector="20px" autoHeight="true" width="auto">
+            <thead>
+                <tr><!-- FIXME: i18n problem: init_auto_l10n() runs
+                        too late to take care of the below elements. -->
+                    <th width="35%" field="type">Title</th>
+                    <th width="25%" field="resource">Barcode</th>
+                    <th width="20%" field="pickup_time">Pickup time</th>
+                    <th width="20%" field="end_time">Due time</th>
+                </tr>
+            </thead>
+        </table>
+        <div class="nice_vertical_padding" id="controls_out_bresv">
+            <form>
+                <input type="button" id="return_button"
+                    class="AUTO_ATTR_VALUE_return"
+                    onclick="p.act_on_selected('return', 'out');" />
+            </form>
+        </div>
+    </div>
+    <div class="nice_vertical_padding" id="contains_in_bresv">
+        <hr />
+        <h3 class="booking AUTO_in_bresv"></h3>
+        <div class="AUTO_no_in_bresv" id="no_in_bresv"></div>
+        <table id="in_bresv" jsId="in_bresv"
+            dojoType="dojox.grid.DataGrid" query="{id: '*'}"
+            rowSelector="20px" autoHeight="true" width="auto">
+            <thead>
+                <tr><!-- FIXME: i18n problem: init_auto_l10n() runs
+                        too late to take care of the below elements. -->
+                    <th width="35%" field="type">Title</th>
+                    <th width="25%" field="resource">Barcode</th>
+                    <th width="20%" field="due_time">Due time</th>
+                    <th width="20%" field="return_time">Return time</th>
+                </tr>
+            </thead>
+        </table>
+    </div>
+    <div class="nice_vertical_padding" id="contains_misc_controls">
+        <hr />
+        <form>
+            <input type="button" class="AUTO_ATTR_VALUE_reset"
+                onclick="p.reset();" />
+        </form>
+    </div>
+</div>
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 b/Open-ILS/web/templates/default/conify/global/booking/reservation.tt2
new file mode 100644 (file)
index 0000000..40a2a5c
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Reservations' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{bresv : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Reservations</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Reservation</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='bresv'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 b/Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2
new file mode 100644 (file)
index 0000000..a879e3f
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Reservation Attribute Value Maps' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{bravm : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Reservation Attribute Value Maps</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Reservation Attribute Value Map</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='bravm'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource.tt2
new file mode 100644 (file)
index 0000000..e5ae269
--- /dev/null
@@ -0,0 +1,43 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Resources' %]
+
+<script src="/opac/common/js/CGI.js" type="text/javascript"></script>
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+    dojo.require('openils.XUL');
+
+    openils.Util.addOnLoad(
+        function() {
+            var search = undefined; // default to all objs
+            if (xulG && xulG.resultant_brsrc) {
+                search = {id: xulG.resultant_brsrc};
+            }
+            ustGrid.loadAll({order_by:{brsrc : 'barcode'}}, search);
+        }
+    );
+</script>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Resources</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Resource</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['owner', 'type', 'barcode',
+                'overbook', 'deposit', 'deposit_amount', 'user_fee']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='brsrc'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2
new file mode 100644 (file)
index 0000000..f26ef6d
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Resource Attributes' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{bra : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Resource Attributes</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Resource Attribute</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='bra'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2
new file mode 100644 (file)
index 0000000..69679af
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Resource Attribute Maps' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{bram : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Resource Attribute Maps</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Resource Attribute Map</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='bram'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2
new file mode 100644 (file)
index 0000000..b4f92ba
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Resource Attribute Values' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{brav : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Resource Attribute Values</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Resource Attribute Value</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='brav'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2
new file mode 100644 (file)
index 0000000..4e8d704
--- /dev/null
@@ -0,0 +1,39 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Resource Types' %]
+
+<script type ="text/javascript">
+    dojo.require('dijit.form.FilteringSelect');
+    dojo.require('openils.widget.AutoGrid');
+
+    openils.Util.addOnLoad(
+        function() {
+            ustGrid.loadAll({order_by:{brt : 'name'}});
+        }
+    );
+</script>
+
+
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" class='oils-header-panel'>
+    <div>Resource Types</div>
+    <div>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.showCreateDialog()'>New Resource Type</button>
+        <button dojoType='dijit.form.Button' onClick='ustGrid.deleteSelected()'>Delete Selected</button>
+    </div>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <table  jsId="ustGrid"
+            autoHeight='true'
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'fine_interval', 'fine_amount',
+                'owner', 'catalog_item', 'transferable', 'record']"
+            query="{name: '*'}"
+            defaultCellWidth='"auto"'
+            fmClass='brt'
+            showPaginator='true'
+            editOnEnter='true'>
+    </table>
+ </div>
+
+[% END %]
index cfa2b9e..aa4f225 100644 (file)
@@ -155,6 +155,10 @@ const api = {
     'FM_AUSP_APPLY' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.apply' },
     'FM_AUSP_REMOVE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.remove' },
     'FM_AUSP_UPDATE_NOTE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.note.update' },
+    'FM_BOOKING_CREATE_BRT_AND_BRSRC' : { 'app' : 'open-ils.booking', 'method' : 'open-ils.booking.create_brt_and_brsrc_from_copies' },
+    'FM_BRESV_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.bresv.atomic' },
+    'FM_BRSRC_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brsrc.atomic' },
+    'FM_BRT_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brt.atomic' },
     'FM_BRE_RETRIEVE_VIA_ID' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.biblio.record.metadata.retrieve', 'secure' : false },
     'FM_BRE_RETRIEVE_VIA_ID.authoritative' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.biblio.record.metadata.retrieve.authoritative', 'secure' : false },
     'FM_BRE_ID_SEARCH_VIA_BARCODE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.biblio.find_by_barcode', 'secure' : false },
@@ -205,6 +209,7 @@ const api = {
     'FM_MP_RETRIEVE_VIA_MBTS_ID.authoritative' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.payment.retrieve.all.authoritative' },
     'FM_MG_CREATE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.grocery.create' },
     'FM_MG_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.grocery.retrieve' },
+    'FM_BRESV_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.booking.reservation.retrieve' },
     'FM_MOBTS_HAVING_BALANCE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance' },
     'FM_MOBTS_HAVING_BALANCE.authoritative' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance.authoritative' },
     'FM_MOBTS_TOTAL_HAVING_BALANCE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance.total' },
index d59c316..29f9bfc 100644 (file)
@@ -116,7 +116,6 @@ main.menu.prototype = {
             );
         }
 
-
         var cmd_map = {
             'cmd_broken' : [
                 ['oncommand'],
@@ -589,6 +588,26 @@ main.menu.prototype = {
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/config/circ_modifier'); }
             ],
+            'cmd_server_admin_booking_resource': [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/booking/resource'); }
+            ],
+            'cmd_server_admin_booking_resource_type': [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/booking/resource_type'); }
+            ],
+            'cmd_server_admin_booking_resource_attr': [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/booking/resource_attr'); }
+            ],
+            'cmd_server_admin_booking_resource_attr_value': [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/booking/resource_attr_value'); }
+            ],
+            'cmd_server_admin_booking_resource_attr_map': [
+                ['oncommand'],
+                function() { open_eg_web_page('conify/global/booking/resource_attr_map'); }
+            ],
             'cmd_acq_view_picklist' : [
                 ['oncommand'],
                 function() { open_eg_web_page('acq/picklist/list', 'menu.cmd_acq_view_picklist.tab'); }
@@ -633,7 +652,81 @@ main.menu.prototype = {
                 ['oncommand'],
                 function() { open_eg_web_page('conify/global/acq/distribution_formula', 'menu.cmd_acq_view_distrib_formula.tab'); }
             ],
-
+            'cmd_booking_reservation' : [
+                ['oncommand'],
+                function() {
+                    obj.set_tab(
+                        "/eg/booking/reservation",
+                        {
+                            "tab_name": offlineStrings.getString(
+                                "menu.cmd_booking_reservation.tab"
+                            ),
+                            "browser": false
+                        },
+                        xulG
+                    );
+                }
+            ],
+            'cmd_booking_pull_list' : [
+                ['oncommand'],
+                function() {
+                    obj.set_tab(
+                        "/eg/booking/pull_list",
+                        {
+                            "tab_name": offlineStrings.getString(
+                                "menu.cmd_booking_pull_list.tab"
+                            ),
+                            "browser": false
+                        },
+                        xulG
+                    );
+                }
+            ],
+            'cmd_booking_capture' : [
+                ['oncommand'],
+                function() {
+                    obj.set_tab(
+                        "/eg/booking/capture",
+                        {
+                            "tab_name": offlineStrings.getString(
+                                "menu.cmd_booking_capture.tab"
+                            ),
+                            "browser": false
+                        },
+                        xulG
+                    );
+                }
+            ],
+            'cmd_booking_reservation_pickup' : [
+                ['oncommand'],
+                function() {
+                    obj.set_tab(
+                        "/eg/booking/pickup",
+                        {
+                            "tab_name": offlineStrings.getString(
+                                "menu.cmd_booking_reservation_pickup.tab"
+                            ),
+                            "browser": false
+                        },
+                        xulG
+                    );
+                }
+            ],
+            'cmd_booking_reservation_return' : [
+                ['oncommand'],
+                function() {
+                    obj.set_tab(
+                        "/eg/booking/return",
+                        {
+                            "tab_name": offlineStrings.getString(
+                                "menu.cmd_booking_reservation_return.tab"
+                            ),
+                            "browser": false
+                        },
+                        xulG
+                    );
+                }
+            ],
             'cmd_reprint' : [
                 ['oncommand'],
                 function() {
index c1a356d..e3daa50 100644 (file)
     <command id="cmd_acq_view_exchange_rate" />
     <command id="cmd_acq_view_distrib_formula" />
 
+    <command id="cmd_booking_reservation" />
+    <command id="cmd_booking_pull_list" />
+    <command id="cmd_booking_capture" />
+    <command id="cmd_booking_reservation_pickup" />
+    <command id="cmd_booking_reservation_return" />
+
 
     <!-- local admin menu commands -->
     <command id="cmd_local_admin_fonts_and_sounds"/>
     <command id="cmd_server_admin_z39_source"/>
     <command id="cmd_server_admin_circ_mod"/>
 
+    <command id="cmd_server_admin_booking_resource" />
+    <command id="cmd_server_admin_booking_resource_type" />
+    <command id="cmd_server_admin_booking_resource_attr" />
+    <command id="cmd_server_admin_booking_resource_attr_value" />
+    <command id="cmd_server_admin_booking_resource_attr_map" />
 </commandset>
 
 
     </menupopup>
 </menu>
 
+<!-- The Booking menu on the main menu -->
+<menu id="main.menu.booking" label="&staff.main.menu.booking.label;" accesskey="&staff.main.menu.booking.accesskey;">
+    <menupopup id="main.menu.booking.popup">
+        <menuitem label="&staff.main.menu.booking.reservation.label;" accesskey="&staff.main.menu.booking.reservation.accesskey;" command="cmd_booking_reservation"/>
+        <menuitem label="&staff.main.menu.booking.pull_list.label;" accesskey="&staff.main.menu.booking.pull_list.accesskey;" command="cmd_booking_pull_list"/>
+        <menuitem label="&staff.main.menu.booking.capture.label;" accesskey="&staff.main.menu.booking.capture.accesskey;" command="cmd_booking_capture"/>
+         <menuitem label="&staff.main.menu.booking.reservation_pickup.label;" accesskey="&staff.main.menu.booking.reservation_pickup.accesskey;" command="cmd_booking_reservation_pickup"/>
+        <menuitem label="&staff.main.menu.booking.reservation_return.label;" accesskey="&staff.main.menu.booking.reservation_return.accesskey;" command="cmd_booking_reservation_return"/>
+    </menupopup>
+</menu>
 
 <!-- The Search menu on the main menu -->
 <menu id="main.menu.search" label="&staff.main.menu.search.label;" accesskey="&staff.main.menu.search.accesskey;">
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.billing_type.label;" command="cmd_server_admin_billing_type"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.z3950_source.label;" command="cmd_server_admin_z39_source"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.circulation_modifier.label;" command="cmd_server_admin_circ_mod"/>
+                <menu id="main.menu.admin.server.booking" label="&staff.main.menu.admin.server_admin.booking.label;" accesskey="&staff.main.menu.admin.server_admin.booking.label;">
+                    <menupopup id="main.menu.admin.server.booking.popup">
+                        <menuitem label="&staff.main.menu.admin.server_admin.booking.resource.label;" command="cmd_server_admin_booking_resource" accesskey="&staff.main.menu.admin.server_admin.booking.resource.accesskey;"/>
+                        <menuitem label="&staff.main.menu.admin.server_admin.booking.resource_type.label;" command="cmd_server_admin_booking_resource_type" accesskey="&staff.main.menu.admin.server_admin.booking.resource_type.accesskey;"/>
+                        <menuitem label="&staff.main.menu.admin.server_admin.booking.resource_attr.label;" command="cmd_server_admin_booking_resource_attr" accesskey="&staff.main.menu.admin.server_admin.booking.resource_attr.accesskey;"/>
+                        <menuitem label="&staff.main.menu.admin.server_admin.booking.resource_attr_value.label;" command="cmd_server_admin_booking_resource_attr_value" accesskey="&staff.main.menu.admin.server_admin.booking.resource_attr_value.accesskey;"/>
+                        <menuitem label="&staff.main.menu.admin.server_admin.booking.resource_attr_map.label;" command="cmd_server_admin_booking_resource_attr_map" accesskey="&staff.main.menu.admin.server_admin.booking.resource_attr_map.accesskey;"/>
+                    </menupopup>
+                </menu>
             </menupopup>
         </menu>
 
index 0358de2..4723b60 100644 (file)
@@ -72,6 +72,7 @@
         <menu id="main.menu.circ" />
         <menu id="main.menu.cat" />
         <menu id="main.menu.acq" />
+        <menu id="main.menu.booking" />
         <spacer flex="1" />
         <menu id="main.menu.admin" />
         <!--
index c9ed325..776a7f4 100644 (file)
@@ -6,7 +6,7 @@ util.functional = {};
 util.functional.EXPORT_OK    = [ 
     'filter_list', 'filter_object', 'find_list', 'find_object', 'map_list', 'map_flat_list', 
     'map_object', 'map_object_to_list', 'convert_object_list_to_hash', 'find_id_object_in_list', 
-    'find_attr_object_in_list', 'walk_tree_preorder',
+    'find_attr_object_in_list', 'walk_tree_preorder', 'unique_list_values',
 ];
 util.functional.EXPORT_TAGS    = { ':all' : util.functional.EXPORT_OK };
 
@@ -134,4 +134,12 @@ util.functional.find_attr_object_in_list = function(list,attr,value) {
     return null;
 }
 
+util.functional.unique_list_values = function(list) {
+    var obj = {};
+    var finished_list = [];
+    for (var i in list) { obj[list[i]] = true; }
+    for (var i in obj) { finished_list.push(i); }
+    return finished_list;
+}
+
 dump('exiting util/functional.js\n');
index 1de9074..d91c458 100644 (file)
@@ -227,6 +227,12 @@ menu.cmd_acq_view_provider.tab=Providers
 menu.cmd_acq_view_currency_type.tab=Currency Types
 menu.cmd_acq_view_exchange_rate.tab=Exchange Rates
 menu.cmd_acq_view_distrib_formula.tab=Distribution Formulas
+menu.cmd_booking_resource.tab=Resources
+menu.cmd_booking_reservation.tab=Reservations
+menu.cmd_booking_reservation_pickup.tab=Reservation Pickup
+menu.cmd_booking_reservation_return.tab=Reservation Return
+menu.cmd_booking_pull_list.tab=Booking Pull List
+menu.cmd_booking_capture.tab=Booking Capture
 menu.local_admin.circ_matrix_matchpoint.tab=Circulation Policies
 menu.local_admin.hold_matrix_matchpoint.tab=Hold Policies
 load_printer_settings_error_description=Printer settings did not load cleanly with this version of Evergreen.  You should reconfigure your printer under Printer Settings Editor.
index 41c86f0..aa01bc5 100644 (file)
@@ -9,6 +9,8 @@
 <html xmlns="http://www.w3.org/1999/xhtml">
     <head>
         <title>&staff.server.admin.index.title;</title>
+        <script type="text/javascript" src="/js/dojo/dojo/dojo.js"
+            djConfig="parseOnLoad: true, isDebug:false"></script>
         <script>
             function _l(l,p) { 
                 var url = l + location.search;
                 location.href = url;
             }
             function getBuildId() { return location.href.match(/\/xul\/(.+?)\/server\//)[1]; }
+
+            function my_init() {
+                try {
+                    dojo.require("dojo.cookie");
+                    window.xulG.auth = {"session": {"key": dojo.cookie("ses")}};
+                } catch(E) { /* XXX ignorable except for booking links */ }
+            }
         </script>
         <style type='text/css'>
             body { background-color: white; }
         </style>
     </head>
-    <body>
+    <body onload="my_init();">
         <center>
             <div style='height: 40px; margin-top: 20px; font-weight: bold; font-size: 14pt;'>
                 &staff.server.admin.index.title;
             <table width='100%'>
                 <thead>
                     <tr>
-                        <th width='30%'>&staff.server.admin.index.workstation_configuration;</th>
-                        <th width='30%'>&staff.server.admin.index.library_configuration;</th>
-                        <th width='30%'>&staff.server.admin.index.maintenance_reports;</th>
+                        <th width='25%'>&staff.server.admin.index.workstation_configuration;</th>
+                        <th width='25%'>&staff.server.admin.index.library_configuration;</th>
+                        <th width='25%'>&staff.server.admin.index.maintenance_reports;</th>
+                        <th width='25%'>&staff.server.admin.index.booking;</th>
                     </tr>
                 </thead>
                 <tbody>
                             <div style='padding: 8px;'>
                                 <a href='javascript:window.xulG.new_tab("/xul/server/admin/transit_list.xul",{"tab_name":"&staff.server.admin.index.transits;"},{});'>&staff.server.admin.index.transit_list;</a>
                             </div>
+                        </td><td>
+                            <div style='padding: 8px;'>
+                                <a href='javascript:window.xulG.new_tab("/eg/booking/reservation",{"tab_name":"&staff.server.admin.index.booking.reservation;","browser":false},window.xulG);'>&staff.server.admin.index.booking.reservation;</a> <span style="color: red">&staff.server.admin.index.testing;</span>
+                            </div>
+                            <div style='padding: 8px;'>
+                                <a href='javascript:window.xulG.new_tab("/eg/booking/pull_list",{"tab_name":"&staff.server.admin.index.booking.pull_list;","browser":false},window.xulG);'>&staff.server.admin.index.booking.pull_list;</a> <span style="color: red">&staff.server.admin.index.testing;</span>
+                            </div>
+                            <div style='padding: 8px;'>
+                                <a href='javascript:window.xulG.new_tab("/eg/booking/capture",{"tab_name":"&staff.server.admin.index.booking.capture;","browser":false},window.xulG);'>&staff.server.admin.index.booking.capture;</a> <span style="color: red">&staff.server.admin.index.testing;</span>
+                            </div>
+                            <div style='padding: 8px;'>
+                                <a href='javascript:window.xulG.new_tab("/eg/booking/pickup",{"tab_name":"&staff.server.admin.index.booking.pickup;","browser":false},window.xulG);'>&staff.server.admin.index.booking.pickup;</a> <span style="color: red">&staff.server.admin.index.testing;</span>
+                            </div>
+                            <div style='padding: 8px;'>
+                                <a href='javascript:window.xulG.new_tab("/eg/booking/return",{"tab_name":"&staff.server.admin.index.booking.return;","browser":false},window.xulG);'>&staff.server.admin.index.booking.return;</a> <span style="color: red">&staff.server.admin.index.testing;</span>
+                            </div>
                         </td>
                     </tr>
                 </tbody>
index bc11a77..61e72ad 100644 (file)
@@ -145,6 +145,56 @@ cat.copy_browser.prototype = {
                                 }
                             }
                         ],
+                        'cmd_create_brt' : [
+                            ['command'],
+                            function() {
+                                JSAN.use("cat.util");
+                                JSAN.use("util.functional");
+
+                                /* Filter selected rows that aren"t copies. */
+                                var list = util.functional.filter_list(
+                                    obj.sel_list,
+                                    function (o) {
+                                        return o.split(/_/)[0] == "acp";
+                                    }
+                                );
+                                var results = cat.util.make_bookable(
+                                    util.functional.map_list(
+                                        list, function (o) {
+                                            return obj.map_acp[o].id();
+                                        }
+                                    )
+                                );
+                                if (results && results["brsrc"]) {
+                                    cat.util.edit_new_brsrc(results["brsrc"]);
+                                }
+                            }
+                        ],
+                        'cmd_book_item_now' : [
+                            ['command'],
+                            function() {
+                                JSAN.use("cat.util");
+                                JSAN.use("util.functional");
+
+                                /* Filter selected rows that aren"t copies. */
+                                var list = util.functional.filter_list(
+                                    obj.sel_list,
+                                    function (o) {
+                                        return o.split(/_/)[0] == "acp";
+                                    }
+                                );
+                                var results = cat.util.make_bookable(
+                                    util.functional.map_list(
+                                        list, function (o) {
+                                            return obj.map_acp[o].id();
+                                        }
+                                    )
+                                );
+                                if (results) {
+                                    cat.util.edit_new_bresv(results);
+                                }
+                            }
+                        ],
                         'cmd_add_items' : [
                             ['command'],
                             function() {
@@ -1589,6 +1639,7 @@ cat.copy_browser.prototype = {
         try {
             var found_aou = false; var found_acn = false; var found_acp = false;
             var found_aou_with_can_have_vols = false;
+            var sel_copy_libs = {};
             for (var i = 0; i < obj.sel_list.length; i++) {
                 var type = obj.sel_list[i].split(/_/)[0];
                 switch(type) {
@@ -1598,7 +1649,15 @@ cat.copy_browser.prototype = {
                         if ( get_bool( obj.data.hash.aout[ org.ou_type() ].can_have_vols() ) ) found_aou_with_can_have_vols = true;
                     break;
                     case 'acn' : found_acn = true; break;
-                    case 'acp' : found_acp = true; break;
+                    case 'acp' :
+                        found_acp = true;
+                        sel_copy_libs[
+                            obj.map_acn[
+                                "acn_" +
+                                obj.map_acp[obj.sel_list[i]].call_number()
+                            ].owning_lib()
+                        ] = true;
+                        break;
                 }
             }
             obj.controller.view.cmd_add_items.setAttribute('disabled','true');
@@ -1615,6 +1674,8 @@ cat.copy_browser.prototype = {
             obj.controller.view.cmd_transfer_volume.setAttribute('disabled','true');
             obj.controller.view.cmd_transfer_items.setAttribute('disabled','true');
             obj.controller.view.sel_copy_details.setAttribute('disabled','true');
+            obj.controller.view.cmd_create_brt.setAttribute('disabled','true');
+            obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
             obj.controller.view.sel_patron.setAttribute('disabled','true');
             obj.controller.view.sel_mark_items_damaged.setAttribute('disabled','true');
             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','true');
@@ -1639,7 +1700,13 @@ cat.copy_browser.prototype = {
                 obj.controller.view.cmd_print_spine_labels.setAttribute('disabled','false');
                 obj.controller.view.cmd_transfer_items.setAttribute('disabled','false');
                 obj.controller.view.sel_copy_details.setAttribute('disabled','false');
+                obj.controller.view.cmd_create_brt.setAttribute('disabled','false');
                 obj.controller.view.sel_patron.setAttribute('disabled','false');
+
+                var L = 0; for (var k in sel_copy_libs) L++;
+                if (L < 2) {
+                    obj.controller.view.cmd_book_item_now.setAttribute('disabled','false');
+                }
             }
         } catch(E) {
             obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.actions.error'),E);
index 6585ea9..1214317 100644 (file)
@@ -73,6 +73,8 @@ vim:noet:sw=4:ts=4:
         <command id="save_columns" />
         <command id="cmd_broken" />
         <command id="sel_copy_details"/>
+        <command id="cmd_create_brt"/>
+        <command id="cmd_book_item_now"/>
         <command id="sel_patron"/>
         <command id="sel_clip" />
         <command id="cmd_clear" />
@@ -103,6 +105,10 @@ vim:noet:sw=4:ts=4:
             <menuitem command="sel_clip" label="&staff.cat.copy_browser.actions.sel_clip.label;" accesskey="&staff.cat.copy_browser.actions.sel_clip.accesskey;"/>
             <menuitem command="cmd_add_items_to_buckets" label="&staff.cat.copy_browser.actions.cmd_add_items_to_buckets.label;" accesskey="&staff.cat.copy_browser.actions.cmd_add_items_to_buckets.accesskey;"/>
             <menuitem command="sel_copy_details" label="&staff.cat.copy_browser.actions.sel_copy_details.label;" accesskey="&staff.cat.copy_browser.actions.sel_copy_details.label;" />
+            <menuseparator/>
+            <menuitem command="cmd_create_brt" label="&staff.cat.copy_browser.actions.cmd_create_brt.label;" accesskey="&staff.cat.copy_browser.actions.cmd_create_brt.accesskey;" />
+            <menuitem command="cmd_book_item_now" label="&staff.cat.copy_browser.actions.cmd_book_item_now.label;" accesskey="&staff.cat.copy_browser.actions.cmd_book_item_now.accesskey;" />
+            <menuseparator/>
             <menuitem command="sel_patron" label="&staff.cat.copy_browser.actions.sel_patron.label;" accesskey="&staff.cat.copy_browser.actions.sel_patron.accesskey;"/>
             <menuseparator/>
             <menuitem command="cmd_edit_items" label="&staff.cat.copy_browser.actions.cmd_edit_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
@@ -154,6 +160,7 @@ vim:noet:sw=4:ts=4:
                         <menuitem command="sel_clip" label="&staff.cat.copy_browser.holdings_maintenance.sel_clip.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_clip.accesskey;"/>
                         <menuitem command="cmd_add_items_to_buckets" label="&staff.cat.copy_browser.holdings_maintenance.add_items_to_bucket.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.add_items_to_bucket.accesskey;"/>
                         <menuitem command="sel_copy_details" label="&staff.cat.copy_browser.holdings_maintenance.sel_copy_details.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_copy_details.accesskey;" />
+                        <menuitem command="cmd_create_brt" label="&staff.cat.copy_browser.holdings_maintenance.cmd_create_brt.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_create_brt.accesskey;" />
                         <menuitem command="sel_patron" label="&staff.cat.copy_browser.holdings_maintenance.sel_patron.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_patron.accesskey;"/>
                         <menuseparator/>
                         <menuitem command="cmd_edit_items" label="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_items.accesskey;"/>
index 938c4b6..bd8289f 100644 (file)
@@ -501,4 +501,77 @@ cat.util.fast_item_add = function(doc_id,cn_label,cp_barcode) {
         if (error) error.standard_unexpected_error_alert('cat.util.fast_item_add',E); else alert('FIXME: ' + E);
     }
 }
+
+cat.util.make_bookable = function(copy_ids) {
+    var results = fieldmapper.standardRequest(
+        ["open-ils.booking", "open-ils.booking.resources.create_from_copies"],
+        [ses(), copy_ids]
+    );
+    if (results == null) {
+        alert(document.getElementById("catStrings").getString(
+            "staff.cat.copy_browser.make_bookable.create_failed_silent"
+        ));
+    }
+    else if (typeof results.ilsevent != "undefined") {
+        alert(document.getElementById("catStrings").getFormattedString(
+            "staff.cat.copy_browser.make_bookable.create_failed",
+            [results.ilsevent, results.textcode, results.desc, results.debug]
+        ));
+    }
+    return results;
+}
+
+cat.util.edit_new_brsrc = function(brsrc_list) {
+    /* Spawn new tab to allow editing new resources. */
+    try {
+        xulG.resultant_brsrc = brsrc_list.map(function(o) { return o[0]; });
+        xulG.new_tab(
+            urls.XUL_BROWSER + "?url=" + window.escape(
+                xulG.url_prefix("/eg/conify/global/booking/resource")
+            ), {
+                "tab_name": offlineStrings.getString(
+                    "menu.cmd_booking_resource.tab"
+                 ),
+                "browser" : true
+            }, {
+                "no_xulG": false,
+                "show_print_button": false,
+                "show_nav_buttons": true,
+                "passthru_content_params": xulG
+            }
+        );
+    } catch(E) {
+        alert(
+            document.getElementById("catStrings").getFormattedString(
+                "staff.cat.copy_browser.make_bookable.newtab_failed"
+            ), E
+        );
+    }
+}
+
+cat.util.edit_new_bresv = function(booking_results) {
+    /* Spawn new tab to allow editing new reservations. */
+    try {
+        if (xulG.auth == undefined) {
+            xulG.auth = {"session": {"key": ses()}};
+        }
+        xulG.bresv_interface_opts = {"booking_results": booking_results};
+        xulG.new_tab(
+            xulG.url_prefix("/eg/booking/reservation"),
+            {
+                "tab_name": offlineStrings.getString(
+                    "menu.cmd_booking_reservation.tab"
+                 ),
+                "browser" : false
+            }, xulG
+        );
+    } catch(E) {
+        alert(
+            document.getElementById("catStrings").getString(
+                "staff.cat.copy_browser.make_bookable.newtab_failed"
+            ) + E
+        );
+    }
+}
+
 dump('exiting cat/util.js\n');
index 7a02828..bf9d084 100644 (file)
@@ -63,6 +63,8 @@ circ.copy_status.prototype = {
                             obj.controller.view.sel_mark_items_damaged.setAttribute('disabled','true');
                             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','true');
                             obj.controller.view.sel_patron.setAttribute('disabled','true');
+                            obj.controller.view.cmd_create_brt.setAttribute('disabled','true');
+                            obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
                             obj.controller.view.sel_spine.setAttribute('disabled','true');
                             obj.controller.view.sel_transit_abort.setAttribute('disabled','true');
                             obj.controller.view.sel_clip.setAttribute('disabled','true');
@@ -87,6 +89,12 @@ circ.copy_status.prototype = {
                             obj.controller.view.sel_copy_details.setAttribute('disabled','false');
                             obj.controller.view.sel_mark_items_damaged.setAttribute('disabled','false');
                             obj.controller.view.sel_mark_items_missing.setAttribute('disabled','false');
+                            if (obj.selected_one_unique_owning_lib()) {
+                                obj.controller.view.cmd_book_item_now.setAttribute('disabled','false');
+                            } else {
+                                obj.controller.view.cmd_book_item_now.setAttribute('disabled','true');
+                            }
+                            obj.controller.view.cmd_create_brt.setAttribute('disabled','false');
                             obj.controller.view.sel_spine.setAttribute('disabled','false');
                             obj.controller.view.sel_transit_abort.setAttribute('disabled','false');
                             obj.controller.view.sel_clip.setAttribute('disabled','false');
@@ -127,6 +135,42 @@ circ.copy_status.prototype = {
                     ],
                     'sel_clip' : [ ['command'], function() { obj.list.clipboard(); obj.controller.view.copy_status_barcode_entry_textbox.focus(); } ],
                     'save_columns' : [ ['command'], function() { obj.list.save_columns(); obj.controller.view.copy_status_barcode_entry_textbox.focus(); } ],
+                    'cmd_create_brt' : [
+                        ['command'],
+                        function() {
+                            JSAN.use("cat.util");
+                            JSAN.use("util.functional");
+
+                            var results = cat.util.make_bookable(
+                                util.functional.map_list(
+                                    obj.selection_list, function (o) {
+                                        return o.copy_id;
+                                    }
+                                )
+                            );
+                            if (results && results["brsrc"]) {
+                                cat.util.edit_new_brsrc(results["brsrc"]);
+                            }
+                        }
+                    ],
+                    'cmd_book_item_now' : [
+                        ['command'],
+                        function() {
+                            JSAN.use("cat.util");
+                            JSAN.use("util.functional");
+
+                            var results = cat.util.make_bookable(
+                                util.functional.map_list(
+                                    obj.selection_list, function (o) {
+                                        return o.copy_id;
+                                    }
+                                )
+                            );
+                            if (results) {
+                                cat.util.edit_new_bresv(results);
+                            }
+                        }
+                    ],
                     'sel_checkin' : [
                         ['command'],
                         function() {
@@ -949,6 +993,15 @@ circ.copy_status.prototype = {
 
     },
 
+    'selected_one_unique_owning_lib': function () {
+        JSAN.use('util.functional');
+        var list = util.functional.map_list(
+            this.selection_list,
+            function(o) { return o.owning_lib; }
+        );
+        return util.functional.unique_list_values(list).length == 1;
+    },
+
     'test_barcode' : function(bc) {
         var obj = this;
         var good = util.barcode.check(bc);
@@ -1041,6 +1094,7 @@ circ.copy_status.prototype = {
                                 'renewable' : details.circ ? 't' : 'f', 
                                 'copy_id' : details.copy.id(), 
                                 'acn_id' : details.volume ? details.volume.id() : -1, 
+                                'owning_lib' : details.volume ? details.volume.owning_lib() : -1, 
                                 'barcode' : barcode, 
                                 'doc_id' : details.mvr ? details.mvr.doc_id() : null  
                             } 
index fd2cb72..4e205a6 100644 (file)
         <command id="cmd_copy_status_upload_file" />
         <command id="cmd_copy_status_print" />
         <command id="save_columns" />
+        <command id="cmd_create_brt" disabled="true"/>
+        <command id="cmd_book_item_now" disabled="true"/>
         <command id="sel_copy_details" disabled="true"/>
         <command id="sel_mark_items_damaged" disabled="true"/>
         <command id="sel_mark_items_missing" disabled="true"/>
index 40417c4..feb5f77 100644 (file)
@@ -18,6 +18,9 @@
         <menuitem command="sel_copy_details" label="&staff.circ.copy_status_overlay.sel_copy_details.label;" accesskey="&staff.circ.copy_status_overlay.sel_copy_details.accesskey;" />
         <menuitem command="sel_patron" label="&staff.circ.copy_status_overlay.sel_patron.label;" accesskey="&staff.circ.copy_status_overlay.sel_patron.accesskey;"/>
         <menuseparator/>
+        <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
+        <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+        <menuseparator/>
         <menuitem command="sel_edit" label="&staff.circ.copy_status_overlay.sel_edit.label;" accesskey="&staff.circ.copy_status_overlay.sel_edit.accesskey;" />
         <menuseparator/>
         <menuitem command="sel_mark_items_damaged" label="&staff.circ.copy_status_overlay.sel_mark_items_damaged.label;" accesskey="&staff.circ.copy_status_overlay.sel_mark_items_damaged.accesskey;"/>
             <menuitem command="sel_copy_details" label="&staff.circ.copy_status_overlay.sel_copy_details.label;" accesskey="&staff.circ.copy_status_overlay.sel_copy_details.accesskey;" />
             <menuitem command="sel_patron" label="&staff.circ.copy_status_overlay.sel_patron.label;" accesskey="&staff.circ.copy_status_overlay.sel_patron.accesskey;"/>
             <menuseparator />
+            <menuitem command="cmd_create_brt" label="&staff.circ.copy_status_overlay.cmd_create_brt.label;" accesskey="&staff.circ.copy_status_overlay.cmd_create_brt.accesskey;"/>
+            <menuitem command="cmd_book_item_now" label="&staff.circ.copy_status_overlay.cmd_book_item_now.label;" accesskey="&staff.circ.copy_status_overlay.cmd_book_item_now.accesskey;"/>
+            <menuseparator />
             <menuitem command="sel_edit" label="&staff.circ.copy_status_overlay.sel_edit.label;" accesskey="&staff.circ.copy_status_overlay.sel_edit.accesskey;" />
             <menuseparator />
             <menuitem command="sel_mark_items_damaged" label="&staff.circ.copy_status_overlay.sel_mark_items_damaged.label;" accesskey="&staff.circ.copy_status_overlay.sel_mark_items_damaged.accesskey;"/>
index b1f0d6c..749db4a 100644 (file)
@@ -11,6 +11,10 @@ staff.cat.bib_brief.noncat.alert=Item not cataloged.
 staff.cat.copy_browser.add_item.title=Add Item
 staff.cat.copy_browser.add_item.error=copy browser -> add copies
 staff.cat.copy_browser.add_items_bucket.error=copy browser -> add copies to bucket
+staff.cat.copy_browser.make_bookable.create_failed_silent=No response from server
+staff.cat.copy_browser.make_bookable.create_failed=Error from server: %1$d %2$s\n%3$s\n%4$s
+staff.cat.copy_browser.make_bookable.newtab_failed=Could not open new tab
+staff.cat.copy_browser.make_bookable.newtab_name=Resources
 staff.cat.copy_browser.replace_barcode.failed=Barcode %1$s not likely replaced.
 staff.cat.copy_browser.replace_barcode.error=copy browser -> replace barcode
 staff.cat.copy_browser.edit_items.error=Copy Browser -> Edit Items
index 31b3d6c..18df786 100644 (file)
@@ -37,6 +37,9 @@ staff.patron.bills.pay.annotate_payment=Please annotate this payment:
 staff.patron.bills.pay.annotate_payment.title=Annotate Payment
 staff.patron.bills.pay.refund_exceeds_desk_payment=%1$s\n\nAnother way to "zero" this transaction is to use Add Billing and add a miscellaneous bill to counter the negative balance.
 staff.patron.bills.pay.payment_failed=Bill payment likely failed
+staff.patron.bills.info_box.label_value.reservation=Reservation
+# 1 - Resource Barcode  2 - Resource Type Name
+staff.patron.bills.info_box.value_format.reservation=%1$s : %2$s
 staff.patron.bills.info_box.label_value.title=Title
 staff.patron.bills.info_box.label_value.type=Type
 staff.patron.bills.info_box.label_value.last_billing=Last Billing:
index 1ac116e..2747797 100644 (file)
@@ -911,6 +911,50 @@ patron.bills.prototype = {
                                 }
                             );
                         break;
+                        case 'reservation':
+                            xt_label.setAttribute( 'value', $("patronStrings").getString('staff.patron.bills.info_box.label_value.reservation') );
+                            obj.network.simple_request(
+                                'FM_BRESV_RETRIEVE_VIA_PCRUD', 
+                                [ ses(), { 'id' : { '=' : my.mobts.id() } } ],
+                                function (req) {
+                                    try {
+                                        var reservation = req.getResultObject()[0];
+                                        if (typeof reservation.ilsevent != 'undefined') { return; }
+                                        obj.network.simple_request(
+                                            'FM_BRSRC_RETRIEVE_VIA_PCRUD',
+                                            [ ses(), { 'id' : { '=' : reservation.target_resource() } } ],
+                                            function (rreq) {
+                                                try {
+                                                    var resource = rreq.getResultObject()[0];
+                                                    if (typeof resource.ilsevent != 'undefined') { return; }
+                                                    obj.network.simple_request(
+                                                        'FM_BRT_RETRIEVE_VIA_PCRUD',
+                                                        [ ses(), { 'id' : { '=' : resource.type() } } ],
+                                                        function (rrreq) {
+                                                            try {
+                                                                var resource_type = rrreq.getResultObject()[0];
+                                                                if (typeof resource_type.ilsevent != 'undefined') { return; }
+                                                                xt_value.appendChild(
+                                                                    document.createTextNode(
+                                                                        $("patronStrings").getFormattedString('staff.patron.bills.info_box.value_format.reservation', [resource.barcode(), resource_type.name()]) 
+                                                                    )
+                                                                );
+                                                            } catch(E) {
+                                                                alert(E);
+                                                            }
+                                                        }
+                                                    );
+                                                } catch(E) {
+                                                    alert(E);
+                                                }
+                                            }
+                                        );
+                                    } catch(E) {
+                                        alert(E);
+                                    }
+                                }
+                            );
+                        break;
                         default:
                                 xt_label.setAttribute( 'value',
                                     my.mvr ? $("patronStrings").getString('staff.patron.bills.info_box.label_value.title') : $("patronStrings").getString('staff.patron.bills.info_box.label_value.type') );
index 277d3e1..cfeb567 100644 (file)
@@ -353,6 +353,69 @@ patron.display.prototype = {
                             }
                         }
                     ],
+                    'cmd_patron_reservation' : [
+                        ['command'],
+                        function(ev) {
+                            if (xulG.auth == undefined) {
+                                xulG.auth = {"session": {"key": ses()}};
+                            }
+                            xulG.bresv_interface_opts = {
+                                "patron_barcode": obj.patron.card().barcode()
+                            };
+                            xulG.new_tab(
+                                "/eg/booking/reservation",
+                                {
+                                    "tab_name": offlineStrings.getString(
+                                        "menu.cmd_booking_reservation.tab"
+                                    ),
+                                    "browser": false
+                                },
+                                xulG
+                            );
+                        }
+                    ],
+                    'cmd_patron_reservation_pickup' : [
+                        ['command'],
+                        function(ev) {
+                            if (xulG.auth == undefined) {
+                                xulG.auth = {"session": {"key": ses()}};
+                            }
+                            xulG.bresv_interface_opts = {
+                                "patron_barcode": obj.patron.card().barcode()
+                            };
+                            xulG.new_tab(
+                                "/eg/booking/pickup",
+                                {
+                                    "tab_name": offlineStrings.getString(
+                                        "menu.cmd_booking_reservation_pickup.tab"
+                                    ),
+                                    "browser": false
+                                },
+                                xulG
+                            );
+                        }
+                    ],
+                    'cmd_patron_reservation_return' : [
+                        ['command'],
+                        function(ev) {
+                            if (xulG.auth == undefined) {
+                                xulG.auth = {"session": {"key": ses()}};
+                            }
+                            xulG.bresv_interface_opts = {
+                                "patron_barcode": obj.patron.card().barcode()
+                            };
+                            xulG.new_tab(
+                                "/eg/booking/return",
+                                {
+                                    "tab_name": offlineStrings.getString(
+                                        "menu.cmd_booking_reservation_return.tab"
+                                    ),
+                                    "browser": false
+                                },
+                                xulG
+                            );
+                        }
+                    ],
                     'cmd_patron_exit' : [
                         ['command'],
                         function(ev) {
index a4ba638..5ed8959 100644 (file)
         <command id="cmd_patron_info_groups" />
         <command id="cmd_patron_other" />
         <command id="cmd_patron_alert" />
+        <command id="cmd_patron_reservation" />
+        <command id="cmd_patron_reservation_pickup" />
+        <command id="cmd_patron_reservation_return" />
         <command id="cmd_patron_exit" />
         <command id="cmd_patron_retrieve" />
         <command id="cmd_patron_merge" />
index 2bf10d2..05eda1d 100644 (file)
         <command id="cmd_patron_info_groups" />
         <command id="cmd_patron_other" />
         <command id="cmd_patron_alert" />
+        <command id="cmd_patron_reservation" />
+        <command id="cmd_patron_reservation_pickup" />
+        <command id="cmd_patron_reservation_return" />
         <command id="cmd_patron_exit" />
         <command id="cmd_patron_retrieve" />
         <command id="cmd_patron_merge" />
index 6b54f53..c341779 100644 (file)
                                     <menuitem label="&staff.patron_navbar.alert;" accesskey="&staff.patron_navbar.alert.accesskey;" command="cmd_patron_alert"/>
                                     <menuitem label="&staff.patron.info.notes.label;" accesskey="&staff.patron.info.notes.accesskey;" command="cmd_patron_info_notes"/>
                                     <menuitem label="&staff.patron.info.stat_cats.label;" accesskey="&staff.patron.info.stat_cats.accesskey;" command="cmd_patron_info_stats"/>
+                                    <menu id="PatronNavBar_other_booking" label="&staff.main.menu.booking.label;" accesskey="&staff.main.menu.booking.accesskey;">
+                                        <menupopup id="PatronNavBar_other_booking_popup">
+                                            <menuitem label="&staff.main.menu.booking.reservation.label;" accesskey="&staff.main.menu.booking.reservation.accesskey;" command="cmd_patron_reservation" />
+                                            <menuitem label="&staff.main.menu.booking.reservation_pickup.label;" accesskey="&staff.main.menu.booking.reservation_pickup.accesskey;" command="cmd_patron_reservation_pickup" />
+                                            <menuitem label="&staff.main.menu.booking.reservation_return.label;" accesskey="&staff.main.menu.booking.reservation_return.accesskey;" command="cmd_patron_reservation_return" />
+                                        </menupopup>
+                                    </menu>
                                     <menuitem label="&staff.patron.info.surveys.label;" accesskey="&staff.patron.info.surveys.accesskey;" command="cmd_patron_info_surveys"/>
                                     <menuitem label="&staff.patron.info.group.label;" accesskey="&staff.patron.info.group.accesskey;" command="cmd_patron_info_groups"/>
                                     <menuitem label="&staff.patron_display.verify_password.label;" accesskey="&staff.patron_display.verify_password.accesskey;" command="cmd_verify_credentials"/>
index 16adf96..3825c88 100644 (file)
                                     <menuitem label="&staff.patron_navbar.alert;" accesskey="&staff.patron_navbar.alert.accesskey;" command="cmd_patron_alert"/>
                                     <menuitem label="&staff.patron.info.notes.label;" accesskey="&staff.patron.info.notes.accesskey;" command="cmd_patron_info_notes"/>
                                     <menuitem label="&staff.patron.info.stat_cats.label;" accesskey="&staff.patron.info.stat_cats.accesskey;" command="cmd_patron_info_stats"/>
+                                    <menu id="PatronNavBar_other_booking" label="&staff.main.menu.booking.label;" accesskey="&staff.main.menu.booking.accesskey;">
+                                        <menupopup id="PatronNavBar_other_booking_popup">
+                                            <menuitem label="&staff.main.menu.booking.reservation.label;" accesskey="&staff.main.menu.booking.reservation.accesskey;" command="cmd_patron_reservation" />
+                                            <menuitem label="&staff.main.menu.booking.reservation_pickup.label;" accesskey="&staff.main.menu.booking.reservation_pickup.accesskey;" command="cmd_patron_reservation_pickup" />
+                                            <menuitem label="&staff.main.menu.booking.reservation_return.label;" accesskey="&staff.main.menu.booking.reservation_return.accesskey;" command="cmd_patron_reservation_return" />
+                                        </menupopup>
+                                    </menu>
                                     <menuitem label="&staff.patron.info.surveys.label;" accesskey="&staff.patron.info.surveys.accesskey;" command="cmd_patron_info_surveys"/>
                                     <menuitem label="&staff.patron.info.group.label;" accesskey="&staff.patron.info.group.accesskey;" command="cmd_patron_info_groups"/>
                                     <menuitem label="&staff.patron_display.verify_password.label;" accesskey="&staff.patron_display.verify_password.accesskey;" command="cmd_verify_credentials"/>