%]</td>
<td>[% money(payment.mp.amount) %]</td>
<td>
- [% IF payment.mp.payment_type == 'credit_card_payment' %]
+ [% IF payment.mp.payment_type == 'credit_card_payment'
+ OR payment.refundable_payment %]
<form action="[% ctx.opac_root %]/biblio/receipt_print" method="POST">
<input type="hidden" name="payment" value="[% payment.mp.id %]" />
<input type="submit" value="[% l('Print') %]" />
--- /dev/null
+-- Deploy kcls-evergreen:lost-paid-receipts-data to pg
+-- requires: lost-paid-receipts
+
+BEGIN;
+
+DO $INSERT$
+BEGIN
+ IF evergreen.insert_on_deploy() THEN
+------------------------------------
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'circ',
+ 'circ.secondary_auth.ldap.server',
+ 'string',
+ 'Secondary Authentication LDAP Server URI',
+ 'URI used by Evergreen to connect to a local LDAP server for ' ||
+ 'secondary authentication. For example: ldaps://ad.example.org:636'
+), (
+ 'circ',
+ 'circ.secondary_auth.ldap.domain',
+ 'string',
+ 'Secondary Authentication LDAP Domain',
+ 'Login domain. This is appended to the usrname during LDAP login.' ||
+ 'This is likely the same as the email domain. For example: example.org'
+), (
+ 'circ',
+ 'circ.secondary_auth.ldap.users_dn',
+ 'string',
+ 'Secondary Authentication LDAP Users DN',
+ 'Example: ou=users,dc=example,dc=org'
+), (
+ 'circ',
+ 'circ.secondary_auth.ldap.users_filter',
+ 'string',
+ 'Secondary Authentication LDAP Users Query Filter',
+ 'Printf-style string for the user query filter. Example: (uid=%s)'
+), (
+ 'circ',
+ 'circ.secondary_auth.ldap.testmode',
+ 'bool',
+ 'Secondary Authentication LDAP Test Mode',
+ 'When set to TRUE, all LDAP authentication requests succeed. ' ||
+ 'For testing purposes only!'
+);
+
+INSERT INTO actor.org_unit_setting (org_unit, name, value) VALUES
+ (1, 'circ.secondary_auth.ldap.server', '"ldaps://ad.kcls.org:636"'),
+ (1, 'circ.secondary_auth.ldap.users_dn', '"ou=staff,dc=ad,dc=kcls,dc=org"'),
+ (1, 'circ.secondary_auth.ldap.users_filter',
+ '"(&(objectCategory=person)(objectClass=user)(userPrincipalName=%s))"'),
+ (1, 'circ.secondary_auth.ldap.domain', '"kcls.org"');
+
+INSERT INTO permission.perm_list (code, description) VALUES
+ ('ADMIN_REFUNDABLE_PAYMENT',
+ 'View/modify refundable payment data for lost/paid receipts');
+
+
+-- Move all perms linked to the "Users" group down to Staff and Patrons
+-- so that Users is empty.
+
+INSERT INTO permission.grp_tree
+ (id, name, parent, perm_interval, application_perm, description)
+VALUES
+ (922, 'Business Office', 920, '1 year',
+ NULL, 'KCLS Business Office Accounts');
+
+-- Give the B.O account the perms it needs to operate.
+
+INSERT INTO permission.grp_perm_map (grp, depth, grantable, perm) VALUES
+ (922, 0, FALSE,
+ (SELECT id FROM permission.perm_list WHERE code = 'STAFF_LOGIN')),
+ (922, 0, FALSE,
+ (SELECT id FROM permission.perm_list WHERE code = 'REGISTER_WORKSTATION')),
+ (922, 0, FALSE,
+ (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_REFUNDABLE_PAYMENT'));
+
+-- Move all 4 "Users" perms to staff.
+UPDATE permission.grp_perm_map SET grp = 3
+ WHERE grp = 1 AND perm IN (95, 32, 6, 5);
+
+-- Then duplicate the 2 missing ones in Patrons as well.
+-- "Patrons" already has perm 95 and don't need 32.
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+ VALUES (2, 6, 0, FALSE), (2, 5, 0, FALSE);
+
+INSERT INTO action_trigger.hook (key, core_type, description)
+ VALUES ('format.mrps.html', 'mrps', 'Refundable Payment Print Receipt');
+
+INSERT INTO action_trigger.event_definition
+ (active, owner, name, hook, validator, reactor, granularity, template)
+VALUES (TRUE, 1,
+ 'Refundable Payment Print Receipt',
+ 'format.mrps.html',
+ 'NOOP_True',
+ 'ProcessTemplate',
+ 'print-on-demand',
+$TEMPLATE$
+[%- USE date ; mrps = target -%]
+[%- USE money=format('%.2f') -%]
+<div>
+ <table><tbody>
+ <tr>
+ <td>Receipt #</td>
+ <td>[% mrps.receipt_code %]</td>
+ </tr>
+ <tr>
+ <td>Patron ID</td>
+ <td>[% mrps.refundable_xact.usr %]</td>
+ </tr>
+ <tr>
+ <td>Patron Name</td>
+ <td>
+ [% mrps.refundable_xact.usr_first_name %]
+ [% mrps.refundable_xact.usr_middle_name %]
+ [% mrps.refundable_xact.usr_family_name %]
+ </td>
+ </tr>
+ <tr>
+ <td style='vertical-align:top'>Patron Address</td>
+ <td>
+ [% mrps.refundable_xact.usr_street1 %]
+ [% mrps.refundable_xact.usr_street2 %]
+ <br/>
+ [% mrps.refundable_xact.usr_city %],
+ [% mrps.refundable_xact.usr_state %]
+ [% mrps.refundable_xact.usr_post_code %]
+ </td>
+ </tr>
+ <tr>
+ <td>Copy Barcode</td>
+ <td>[% mrps.refundable_xact.copy_barcode %]</td>
+ </tr>
+ <tr>
+ <td>Copy Price</td>
+ <td>$[% money(mrps.refundable_xact.item_price) %]</td>
+ </tr>
+ <tr>
+ <td>Call Number</td>
+ <td>[% mrps.refundable_xact.call_number %]</td>
+ </tr>
+ <tr>
+ <td>Title</td>
+ <td>[% mrps.refundable_xact.title %]</td>
+ </tr>
+ <tr>
+ <td>Payment Amount</td>
+ <td>$[% money(mrps.amount) %]</td>
+ </tr>
+ <tr>
+ <td>Payment Type</td>
+ <td>
+ [% SWITCH mrps.payment_type -%]
+ [% CASE "cash_payment" %]Cash
+ [% CASE "check_payment" %]Check
+ [% CASE "credit_card_payment" %]Credit Card
+ [% CASE ; mrps.payment_type %]
+ [% END %]
+ </td>
+ </tr>
+ <tr>
+ <td>Paid At</td>
+ <td>[% mrps.payment_ou.name %]</td>
+ </tr>
+ <tr>
+ <td>Payment Accepted By</td>
+ <td>[% helpers.name_to_initials(mrps.staff_name) %]</td>
+ </tr>
+ <tr>
+ <td>Payment Date</td>
+ <td>[% date.format(helpers.format_date(mrps.payment_time), '%m/%d/%Y %l:%M %p') %]</td>
+ </tr>
+ <tr>
+ <td>Receipt Printed On</td>
+ <td>[% date.format(date.now, '%m/%d/%Y %l:%M %p') %]</td>
+ </tr>
+ </tbody></table>
+ <div>
+ <p>
+ If the items listed on this receipt are returned within 12 months of
+ the receipt date (see exclusions below), you may request a refund
+ within 12 months of the receipt date for the cost of the item minus
+ any outstanding charges on your account. The usual overdue fee will
+ still be in effect for the returned item. You may choose to pay this
+ overdue fee at the time you request a refund or have the overdue fee
+ deducted from the amount of the refund. Patrons requesting a refund
+ must present this receipt at any KCLS location.
+ </p>
+ <p>
+ Refunds are not available for parts and pieces, overdue fines, or items
+ that do not display a specific title in My Account. For a list of
+ refundable and nonrefundable items, visit
+ <a href='http://www.kcls.org/usingthelibrary/borrowing/'>
+ http://kcls.org/faq/borrowing/#faq_1766
+ </a>
+ </p>
+ **END OF RECEIPT**
+ </div>
+</div>
+$TEMPLATE$
+);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+ (
+ (SELECT id FROM action_trigger.event_definition
+ WHERE hook = 'format.mrps.html'),
+ 'refundable_xact'
+ ), (
+ (SELECT id FROM action_trigger.event_definition
+ WHERE hook = 'format.mrps.html'),
+ 'payment_ou'
+ )
+
+;
+
+---------------------------------------------------------------
+
+INSERT INTO action_trigger.hook (key, core_type, description)
+ VALUES ('format.mrps.email', 'mrps', 'Refundable Payment Email Receipt');
+
+INSERT INTO action_trigger.event_definition
+ (active, owner, name, hook, validator, reactor, granularity, template)
+VALUES (TRUE, 1,
+ 'Refundable Payment Email Receipt',
+ 'format.mrps.email',
+ 'NOOP_True',
+ 'SendEmail',
+ 'print-on-demand',
+$TEMPLATE$
+[%- USE date;
+ USE money=format('%.2f');
+ mrps = target;
+ user = mrps.refundable_xact.usr;
+ lib = mrps.payment_ou;
+-%]
+To: [%- params.recipient_email || user.email %]
+From: [%-
+ params.sender_email
+ || helpers.get_org_setting(lib.id, 'org.bounced_emails')
+ || lib.email
+ || default_sender
+%]
+Subject: KCLS Payment Receipt
+
+[% mrps.refundable_xact.usr_first_name %] [% mrps.refundable_xact.usr_middle_name %] [% mrps.refundable_xact.usr_family_name %]
+[% mrps.refundable_xact.usr_street1 %] [% mrps.refundable_xact.usr_street2 %]
+[% mrps.refundable_xact.usr_city %], [% mrps.refundable_xact.usr_state %] [% mrps.refundable_xact.usr_post_code %]
+
+Receipt #: [% mrps.receipt_code %]
+Patron ID: [% mrps.refundable_xact.usr.id %]
+Copy Barcode: [% mrps.refundable_xact.copy_barcode %]
+Copy Price: $[% money(mrps.refundable_xact.item_price) %]
+Call Number: [% mrps.refundable_xact.call_number %]
+Title: [% mrps.refundable_xact.title %]
+Payment Amount: $[% money(mrps.amount) %]
+Payment Type: [% SWITCH mrps.payment_type -%]
+ [%- CASE "cash_payment" %]Cash
+ [%- CASE "check_payment" %]Check
+ [%- CASE "credit_card_payment" %]Credit Card
+ [%- CASE ; mrps.payment_type -%]
+[%- END %]
+Paid At: [% mrps.payment_ou.name %]
+Payment Accepted By: [% helpers.name_to_initials(mrps.staff_name) %]
+Payment Date: [% date.format(helpers.format_date(mrps.payment_time), '%m/%d/%Y %l:%M %p') %]
+Receipt Printed On: [% date.format(date.now, '%m/%d/%Y %l:%M %p') %]
+
+If the items listed on this receipt are returned within 12 months of
+the receipt date (see exclusions below), you may request a refund
+within 12 months of the receipt date for the cost of the item minus
+any outstanding charges on your account. The usual overdue fee will
+still be in effect for the returned item. You may choose to pay this
+overdue fee at the time you request a refund or have the overdue fee
+deducted from the amount of the refund. Patrons requesting a refund
+must present this receipt at any KCLS location.
+
+Refunds are not available for parts and pieces, overdue fines, or items
+that do not display a specific title in My Account. For a list of
+refundable and nonrefundable items, visit
+<a href='http://www.kcls.org/usingthelibrary/borrowing/'>
+http://kcls.org/faq/borrowing/#faq_1766
+
+**END OF RECEIPT**
+
+$TEMPLATE$
+);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+ (
+ (SELECT id FROM action_trigger.event_definition
+ WHERE hook = 'format.mrps.email'),
+ 'refundable_xact.usr'
+ ), (
+ (SELECT id FROM action_trigger.event_definition
+ WHERE hook = 'format.mrps.email'),
+ 'payment_ou'
+ )
+
+;
+
+------------------------------------
+ END IF; -- insert_on_deploy
+END $INSERT$;
+
+
+COMMIT;
--- /dev/null
+
+BEGIN;
+
+-- Staff usr ID can be accessed from money.payment.accepting_usr.
+-- Create time comes from money.payment.payment_ts.
+
+-- money.payment.xact for circ payments always points to a circ or
+-- an aged circ.
+
+-- When a patron is purged, non-circ transaction are deleted,
+-- but any payments linked to those transactions stick around!
+
+-- Patron ID not tracked since it's accessible through the transaction
+-- and this way we don't have to NULL-ify the field if the patron is purged.
+
+CREATE TABLE money.refundable_xact (
+ id SERIAL PRIMARY KEY,
+ xact BIGINT, -- REFERENCES money.billable_xact (id) (parent table)
+ action_date TIMESTAMP WITH TIME ZONE,
+ action_by INTEGER REFERENCES actor.usr (id),
+ item_price NUMERIC(8,2) NOT NULL DEFAULT 0,
+ refund_amount NUMERIC(8,2),
+ rejected BOOLEAN NOT NULL DEFAULT FALSE,
+ notes TEXT,
+ usr_first_name TEXT NOT NULL,
+ usr_middle_name TEXT,
+ usr_family_name TEXT NOT NULL,
+ usr_barcode TEXT NOT NULL,
+ usr_street1 TEXT NOT NULL,
+ usr_street2 TEXT,
+ usr_city TEXT NOT NULL,
+ usr_state TEXT NOT NULL,
+ usr_post_code TEXT NOT NULL
+);
+
+CREATE INDEX m_r_x_xact_idx ON money.refundable_xact (xact);
+
+CREATE TABLE money.refundable_payment (
+ id SERIAL PRIMARY KEY,
+ refundable_xact BIGINT NOT NULL REFERENCES money.refundable_xact (id),
+ payment BIGINT NOT NULL, -- REFERENCES money.payment (id) (parent table)
+ payment_ou BIGINT NOT NULL REFERENCES actor.org_unit (id),
+ final_payment BOOLEAN NOT NULL DEFAULT TRUE,
+ receipt_number INTEGER NOT NULL, -- trigger generated
+ refunded_via TEXT,
+ staff_name TEXT NOT NULL,
+ staff_email TEXT NOT NULL,
+ CONSTRAINT valid_refunded_via CHECK
+ (refunded_via IS NULL OR refunded_via IN ('check', 'credit_card'))
+);
+
+CREATE INDEX m_r_p_payment_idx ON money.refundable_payment (payment);
+
+CREATE OR REPLACE FUNCTION gen_refundable_payment_number ()
+ RETURNS TRIGGER AS $_$
+BEGIN
+ PERFORM TRUE FROM money.credit_card_payment WHERE id = NEW.payment;
+ IF FOUND THEN
+ -- credit card payments do not get a special receipt number.
+ NEW.receipt_number := 0;
+ ELSE
+ SELECT INTO NEW.receipt_number COALESCE(MAX(receipt_number), 0) + 1
+ FROM money.refundable_payment WHERE payment_ou = NEW.payment_ou;
+ END IF;
+
+ RETURN NEW;
+END;
+$_$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER gen_refundable_payment_number_tgr
+ BEFORE INSERT ON money.refundable_payment
+ FOR EACH ROW EXECUTE PROCEDURE gen_refundable_payment_number();
+
+CREATE OR REPLACE VIEW money.refundable_xact_summary AS
+ SELECT
+ xact.*,
+ acp.id AS copy,
+ acp.barcode AS copy_barcode,
+ acn.label AS call_number,
+ CASE WHEN acn.id = -1
+ THEN acp.dummy_title
+ ELSE rsr.title
+ END AS title,
+ circ.usr AS usr, -- may be null
+ circ.xact_start AS xact_start,
+ circ.xact_finish AS xact_finish,
+ summary.total_owed AS total_owed,
+ summary.balance_owed AS balance_owed,
+ refundable_paid.amount::NUMERIC(8,2) AS refundable_paid,
+ total_paid.amount::NUMERIC(8,2) AS total_paid,
+ total_refunded.amount::NUMERIC(8,2) AS total_refunded,
+ refundable_payment_count.count AS num_refundable_payments
+ FROM money.refundable_xact xact
+ JOIN action.all_circulation circ ON (circ.id = xact.xact)
+ JOIN asset.copy acp ON (acp.id = circ.target_copy)
+ JOIN asset.call_number acn ON (acn.id = acp.call_number)
+ JOIN reporter.materialized_simple_record rsr ON (rsr.id = acn.record)
+ JOIN money.materialized_billable_xact_summary summary
+ ON (summary.id = xact.id)
+ JOIN (
+ SELECT pay.xact, SUM(pay.amount) amount
+ FROM money.payment pay WHERE amount > 0 GROUP BY 1
+ ) total_paid ON (total_paid.xact = xact.xact)
+ JOIN (
+ SELECT mrp.refundable_xact, SUM(pay.amount) AS amount
+ FROM money.refundable_payment mrp
+ JOIN money.payment pay ON (mrp.payment = pay.id)
+ GROUP BY 1
+ ) refundable_paid ON (refundable_paid.refundable_xact = xact.id)
+ LEFT JOIN (
+ -- refunds are negative payments, negate the amount
+ SELECT pay.xact, -SUM(pay.amount) amount
+ FROM money.cash_payment pay
+ WHERE amount < 0
+ GROUP BY 1
+ ) total_refunded ON (total_refunded.xact = xact.xact)
+ JOIN (
+ SELECT COUNT(*) AS count, mrp.refundable_xact
+ FROM money.refundable_payment mrp
+ GROUP BY 2
+ ) refundable_payment_count
+ ON (refundable_payment_count.refundable_xact = xact.id)
+;
+
+CREATE OR REPLACE VIEW money.refundable_payment_summary AS
+ SELECT
+ mrp.*,
+ aou.shortname ||
+ LPAD(mrp.receipt_number::TEXT, 6, '0') AS receipt_code,
+ pay.payment_ts AS payment_time,
+ pay.amount AS amount,
+ pview.payment_type,
+ aws.name AS workstation
+ FROM money.refundable_payment mrp
+ JOIN money.payment pay ON (pay.id = mrp.payment)
+ JOIN actor.org_unit aou ON (aou.id = mrp.payment_ou)
+ JOIN money.payment_view pview ON (pview.id = pay.id)
+ LEFT JOIN money.cash_payment cash ON (cash.id = pay.id)
+ LEFT JOIN actor.workstation aws ON (aws.id = cash.cash_drawer)
+;
+
+COMMIT;
+
--- /dev/null
+-- Revert kcls-evergreen:lost-paid-receipts-data from pg
+
+BEGIN;
+
+DELETE FROM permission.grp_perm_map WHERE perm IN (
+ SELECT id FROM permission.perm_list
+ WHERE code IN ('ADMIN_REFUNDABLE_PAYMENT')
+);
+
+DELETE FROM permission.perm_list
+ WHERE code IN ('ADMIN_REFUNDABLE_PAYMENT');
+
+-- B.O. perms
+DELETE FROM permission.grp_perm_map WHERE grp = 922;
+
+-- Vendor, Business Office
+DELETE FROM permission.grp_tree WHERE id IN (922);
+
+-- Move the 4 global perms back to Users
+UPDATE permission.grp_perm_map SET grp = 1
+ WHERE grp = 3 AND perm IN (95, 32, 6, 5);
+
+DELETE FROM permission.grp_perm_map WHERE grp = 2 AND perm IN (6, 5);
+
+DELETE FROM action_trigger.event WHERE event_def IN (
+ SELECT id FROM action_trigger.event_definition
+ WHERE hook IN ('format.mrps.html', 'format.mrps.email')
+);
+
+DELETE FROM action_trigger.environment WHERE event_def IN (
+ SELECT id FROM action_trigger.event_definition
+ WHERE hook IN ('format.mrps.html', 'format.mrps.email')
+);
+
+DELETE FROM action_trigger.event_definition WHERE id IN (
+ SELECT id FROM action_trigger.event_definition
+ WHERE hook IN ('format.mrps.html', 'format.mrps.email')
+);
+
+DELETE FROM action_trigger.hook
+ WHERE key IN ('format.mrps.html', 'format.mrps.email');
+
+DELETE FROM actor.org_unit_setting
+ WHERE name ~ '^circ.secondary_auth.ldap';
+
+DELETE FROM config.org_unit_setting_type_log
+ WHERE field_name ~ '^circ.secondary_auth.ldap';
+
+DELETE FROM config.org_unit_setting_type
+ WHERE name ~ '^circ.secondary_auth.ldap';
+
+COMMIT;
+
--- /dev/null
+-- Revert kcls-evergreen:lost-paid-receipts from pg
+
+BEGIN;
+
+DROP VIEW IF EXISTS money.refundable_payment_summary;
+DROP VIEW IF EXISTS money.refundable_xact_summary;
+DROP INDEX IF EXISTS m_r_p_payment_idx;
+DROP INDEX IF EXISTS m_r_x_xact_idx;
+DROP TABLE IF EXISTS money.refundable_payment;
+DROP TABLE IF EXISTS money.refundable_xact;
+
+COMMIT;
remove-gender [ecard-data] 2018-06-06T14:44:36Z Bill Erickson,,, <berick@kcls-dev-local> # Remove gender field/data
ecard-notice-validator [remove-gender] 2018-07-26T14:33:57Z Bill Erickson,,, <berick@kcls-dev-local> # eCard UMS notice validator
aged-billings-payments [ecard-notice-validator] 2018-09-24T18:00:57Z Bill Erickson,,, <berick@kcls-dev> # Aged money/billing schema and data
+lost-paid-receipts [2.9-to-2.10-upgrade-reingest] 2017-07-03T20:10:59Z Bill Erickson,,, <berick@kcls-dev-local> # Lost/Paid tracking and receipts
+lost-paid-receipts-data [lost-paid-receipts] 2017-08-02T15:28:08Z Bill Erickson,,, <berick@kcls-dev-local> # Lost/Paid permissions and receipts
stock-browse-schema [ecard-notice-validator] 2018-08-31T15:22:58Z Bill Erickson,,, <berick@kcls-dev-local> # Recover stock browse data tables, etc.
stock-browse-headings-report [stock-browse-schema] 2018-10-04T15:56:18Z Bill Erickson,,, <berick@kcls-dev> # New heading report updates for stock browse
stock-browse-cleanup [stock-browse-schema] 2018-10-03T18:05:49Z Bill Erickson,,, <berick@kcls-dev> # Delete old browse data
--- /dev/null
+-- Verify kcls-evergreen:lost-paid-receipts-data on pg
+
+BEGIN;
+
+-- XXX Add verifications here.
+
+ROLLBACK;
--- /dev/null
+-- Verify kcls-evergreen:lost-paid-receipts on pg
+
+BEGIN;
+
+-- XXX Add verifications here.
+
+ROLLBACK;
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
- <retrieve permission="VIEW_USER_TRANSACTIONS">
+ <retrieve permission="VIEW_USER_TRANSACTIONS ADMIN_REFUNDABLE_PAYMENT">
<context link="usr" field="home_ou" />
</retrieve>
</actions>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
- <retrieve permission="VIEW_USER_TRANSACTIONS">
+ <retrieve permission="VIEW_USER_TRANSACTIONS ADMIN_REFUNDABLE_PAYMENT">
<context link="xact" jump="usr" field="home_ou"/>
</retrieve>
</actions>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
- <retrieve permission="VIEW_USER_TRANSACTIONS">
+ <retrieve permission="VIEW_USER_TRANSACTIONS ADMIN_REFUNDABLE_PAYMENT">
<context link="xact" jump="usr" field="home_ou"/>
</retrieve>
</actions>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
<create permission="CREATE_BILLING_TYPE" context_field="owner"/>
- <retrieve permission="VIEW_BILLING_TYPE CREATE_BILLING_TYPE UPDATE_BILLING_TYPE DELETE_BILLING_TYPE" context_field="owner"/>
+ <retrieve permission="VIEW_BILLING_TYPE CREATE_BILLING_TYPE UPDATE_BILLING_TYPE DELETE_BILLING_TYPE ADMIN_REFUNDABLE_PAYMENT" context_field="owner"/>
<update permission="UPDATE_BILLING_TYPE" context_field="owner"/>
<delete permission="DELETE_BILLING_TYPE" context_field="owner"/>
</actions>
</class>
<!-- ********************************************************************************************************************* -->
+
+ <!-- KCLS refundable payments classes -->
+
+ <class id="mrx" controller="open-ils.cstore"
+ oils_obj:fieldmapper="money::refundable_xact"
+ oils_persist:tablename="money.refundable_xact"
+ reporter:label="Refundable Transaction">
+ <fields oils_persist:primary="id"
+ oils_persist:sequence="money.refundable_xact_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID"/>
+ <field name="xact" reporter:datatype="link" reporter:label="Transaction ID"/>
+ <field name="action_date" reporter:datatype="timestamp" reporter:label="Action Date"/>
+ <field name="action_by" reporter:datatype="link" reporter:label="Action By"/>
+ <field name="item_price" reporter:datatype="money" reporter:label="Item Price"/>
+ <field name="refund_amount" reporter:datatype="money" reporter:label="Refund Amount"/>
+ <field name="rejected" reporter:datatype="bool" reporter:label="Rejected"/>
+ <field name="notes" reporter:datatype="text" reporter:label="Notes"/>
+ <field name="usr_first_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron First Name"/>
+ <field name="usr_middle_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Middle Name"/>
+ <field name="usr_family_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Last Name"/>
+ <field name="usr_barcode" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Barcode"/>
+ <field name="usr_street1" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Street1"/>
+ <field name="usr_street2" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Street2"/>
+ <field name="usr_city" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="City"/>
+ <field name="usr_state" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="State"/>
+ <field name="usr_post_code" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Post Code"/>
+ </fields>
+ </class>
+ <class id="mrxs" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="money::refundable_xact_summary"
+ oils_persist:tablename="money.refundable_xact_summary"
+ reporter:label="Refundable Transaction Summary"
+ oils_persist:readonly="true">
+ <fields oils_persist:primary="id"
+ oils_persist:sequence="money.refundable_xact_id_seq">
+ <field name="id" reporter:datatype="id" reporter:label="ID"/>
+ <field name="xact" reporter:datatype="link" reporter:label="Transaction ID"/>
+ <field name="action_date" reporter:datatype="timestamp" reporter:label="Action Date"/>
+ <field name="action_by" reporter:datatype="link" reporter:label="Action By"/>
+ <field name="item_price" reporter:datatype="money" reporter:label="Item Price"/>
+ <field name="refund_amount" reporter:datatype="money" reporter:label="Refund Amount"/>
+ <field name="rejected" reporter:datatype="bool" reporter:label="Rejected"/>
+ <field name="notes" reporter:datatype="text" reporter:label="Notes"/>
+ <field name="usr_first_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron First Name"/>
+ <field name="usr_middle_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Middle Name"/>
+ <field name="usr_family_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Last Name"/>
+ <field name="usr_barcode" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Patron Barcode"/>
+ <field name="usr_street1" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Street1"/>
+ <field name="usr_street2" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Street2"/>
+ <field name="usr_city" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="City"/>
+ <field name="usr_state" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="State"/>
+ <field name="usr_post_code" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Post Code"/>
+ <field name="usr" reporter:datatype="link" reporter:label="User ID" suppress_controller="open-ils.reporter-store"/>
+ <field name="xact_start" reporter:datatype="timestamp" reporter:label="Transaction Start Date"/>
+ <field name="xact_finish" reporter:datatype="timestamp" reporter:label="Transaction End Date"/>
+ <field name="total_owed" reporter:datatype="money" reporter:label="Total Owed"/>
+ <field name="balance_owed" reporter:datatype="money" reporter:label="Balance Owed"/>
+ <field name="refundable_paid" reporter:datatype="money" reporter:label="Refundable Amount Paid"/>
+ <field name="total_paid" reporter:datatype="money" reporter:label="Total Paid"/>
+ <field name="total_refunded" reporter:datatype="money" reporter:label="Total Amount Refunded (By Staff)"/>
+ <field name="num_refundable_payments" reporter:datatype="int" reporter:label="Refundable Payment Count"/>
+ <field name="copy" reporter:datatype="link" reporter:label="Copy ID"/>
+ <field name="copy_barcode" reporter:datatype="text" reporter:label="Copy Barcode"/>
+ <field name="call_number" reporter:datatype="text" reporter:label="Call Number"/>
+ <field name="title" reporter:datatype="text" reporter:label="Title"/>
+ <field name="refundable_payments" reporter:datatype="link" oils_persist:virtual="true"/>
+ </fields>
+ <links>
+ <link field="xact" reltype="has_a" key="id" map="" class="mbt"/>
+ <link field="refundable_payments" reltype="has_many" key="refundable_xact" map="" class="mrps"/>
+ <link field="copy" reltype="has_a" key="id" map="" class="acp"/>
+ <link field="usr" reltype="has_a" key="id" map="" class="au"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_REFUNDABLE_PAYMENT" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="mrp" controller="open-ils.cstore"
+ oils_obj:fieldmapper="money::refundable_payment"
+ oils_persist:tablename="money.refundable_payment"
+ reporter:label="Refundable Payment">
+ <fields oils_persist:primary="id"
+ oils_persist:sequence="money.refundable_payment_id_seq">
+ <field name="id" reporter:datatype="id" />
+ <field name="refundable_xact" reporter:datatype="link" reporter:label="Refundable Transaction ID"/>
+ <field name="payment" reporter:datatype="link" reporter:label="Payment ID"/>
+ <field name="payment_ou" reporter:datatype="link" reporter:label="Payment Library"/>
+ <field name="final_payment" reporter:datatype="bool" reporter:label="Final Payment"/>
+ <field name="receipt_number" reporter:datatype="text" reporter:label="Receipt Code"/>
+ <field name="refunded_via" reporter:datatype="text" reporter:label="Refunded Via"/>
+ <field name="staff_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Staff Name"/>
+ <field name="staff_email" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Staff Email"/>
+ </fields>
+ <links>
+ <link field="payment" reltype="has_a" key="id" map="" class="mp"/>
+ <link field="payment_ou" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="refundable_xact" reltype="has_a" key="id" map="" class="mrx"/>
+ <link field="action_by" reltype="has_a" key="id" map="" class="au"/>
+ </links>
+ </class>
+ <class id="mrps" controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="money::refundable_payment_summary"
+ oils_persist:tablename="money.refundable_payment_summary"
+ reporter:label="Refundable Payment Summary">
+ <fields oils_persist:primary="id"
+ oils_persist:sequence="money.refundable_payment_id_seq">
+ <field name="id" reporter:datatype="id" />
+ <field name="refundable_xact" reporter:datatype="link" reporter:label="Refundable Transaction ID"/>
+ <field name="payment" reporter:datatype="link" reporter:label="Payment ID"/>
+ <field name="payment_ou" reporter:datatype="link" reporter:label="Payment Library"/>
+ <field name="final_payment" reporter:datatype="bool" reporter:label="Final Payment"/>
+ <field name="receipt_number" reporter:datatype="text" reporter:label="Receipt Number"/>
+ <field name="refunded_via" reporter:datatype="text" reporter:label="Refunded Via"/>
+ <field name="staff_name" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Staff First Name"/>
+ <field name="staff_email" reporter:datatype="text" suppress_controller="open-ils.reporter-store" reporter:label="Staff Email"/>
+ <field name="receipt_code" reporter:datatype="text" reporter:label="Receipt Code"/>
+ <field name="payment_time" reporter:datatype="timestamp" reporter:label="Payment Time"/>
+ <field name="amount" reporter:datatype="money" reporter:label="Amount Paid"/>
+ <field name="payment_type" reporter:datatype="text" reporter:label="Payment Type"/>
+ <field name="workstation" reporter:datatype="text" reporter:label="Payment Workstation"/>
+ </fields>
+ <links>
+ <link field="payment" reltype="has_a" key="id" map="" class="mp"/>
+ <link field="payment_ou" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="refundable_xact" reltype="has_a" key="id" map="" class="mrxs"/>
+ <link field="action_by" reltype="has_a" key="id" map="" class="au"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve permission="ADMIN_REFUNDABLE_PAYMENT" global_required="true"/>
+ </actions>
+ </permacrud>
+ </class>
+
</IDL>
<!--
use OpenILS::Application::Circ::NonCat;
use OpenILS::Application::Circ::CopyLocations;
use OpenILS::Application::Circ::CircCommon;
+use OpenILS::Application::Circ::RefundablePayment;
use DateTime;
use DateTime::Format::ISO8601;
use OpenILS::Utils::DateTime qw/:datetime/;
use DateTime::Format::ISO8601;
my $parser = DateTime::Format::ISO8601->new;
+use OpenILS::Application::Circ::RefundablePayment;
sub get_processor_settings {
my $e = shift;
[trans_id, amt],
[...]
],
+ refundable_args : {
+ secondary_auth_key : $key,
+ transactions : [
+ {xact : id},
+ ...
+ ]
+ }
}/, type => 'hash'
},
{
my $drawer = $e->requestor->wsid;
my $note = $payments->{note};
my $cc_args = $payments->{cc_args};
+ my $refundable_args = $payments->{refundable_args} || {};
my $check_number = $payments->{check_number};
my $total_paid = 0;
my $this_ou = $e->requestor->ws_ou || $e->requestor->home_ou;
my %orgs;
+ return OpenILS::Event->new('BAD_PARAMS', note =>
+ 'Secondary auth key required for refundable payment tracking')
+ if (
+ $refundable_args->{transactions}
+ && !$refundable_args->{secondary_auth_key}
+ );
# unless/until determined by payment processor API
my ($approval_code, $cc_processor, $cc_order_number) = (undef,undef,undef, undef);
}
push(@payment_ids, $payment->id);
+
+ if ($refundable_args->{transactions}) {
+ # If this is one of the refundable payments, add the payment
+ # ID to the set of per-payment options provided by the caller.
+ my ($ref_data) = grep
+ {$_->{xact} == $transid} @{$refundable_args->{transactions}};
+ $ref_data->{payment_id} = $payment->id if $ref_data;
+ }
}
my $evt = _update_patron_credit($e, $patron, $credit);
}
}
+ my $refundable_payments = [];
+ if ($refundable_args->{transactions}) {
+ my $authkey = $refundable_args->{secondary_auth_key};
+
+ for my $pay_args (@{$refundable_args->{transactions}}) {
+ my $pay_id = $pay_args->{payment_id};
+ my $evt = OpenILS::Application::Circ::RefundablePayment
+ ->create_refundable_payment(
+ $e, $authkey, $pay_id, $pay_args, $refundable_payments);
+
+ if ($evt) {
+ $logger->error(
+ "Error tracking refundable payment data for payment $pay_id");
+ return $e->die_event;
+ }
+ }
+ }
+
# update the user to create a new last_xact_id
$e->update_actor_user($patron) or return $e->die_event;
$patron = $e->retrieve_actor_user($patron) or return $e->die_event;
if $user_id == $e->requestor->id;
$U->log_user_activity($user_id, '', 'payment');
- return {last_xact_id => $patron->last_xact_id, payments => \@payment_ids};
+ return {
+ last_xact_id => $patron->last_xact_id,
+ payments => \@payment_ids,
+ refundable_payments => $refundable_payments
+ };
}
sub _recording_failure {
--- /dev/null
+# ---------------------------------------------------------------
+# Copyright (C) 2017 King County Library System
+# Bill Erickson <berickxx@gmail.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.
+# ---------------------------------------------------------------
+#
+# KCLS JBAS-1306 Lost+Paid Refundable Payments Tracking
+#
+# ---------------------------------------------------------------
+package OpenILS::Application::Circ::RefundablePayment;
+use strict; use warnings;
+use base qw/OpenILS::Application/;
+use OpenSRF::Utils::Cache;
+use OpenSRF::Utils::Logger qw/:logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Event;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use Digest::MD5 qw(md5_hex);
+use Net::LDAP;
+use Net::LDAP::Constant qw[LDAP_INVALID_CREDENTIALS];
+my $U = "OpenILS::Application::AppUtils";
+
+my $ldap_timeout = 30; # server connection timeout
+my $ldap_key_timeout = 30; # 2ndary auth token timeout
+my $ldap_key_prefix = 'ldap_auth_';
+
+# XXX
+# XXX Add this API to the log_protect section in opensrf_core.xml
+# XXX
+
+__PACKAGE__->register_method(
+ method => 'authenticate_ldap',
+ api_name => 'open-ils.circ.staff.secondary_auth.ldap',
+ signature => {
+ desc => q/Verifies secondary credentials via LDAP/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Username. No domain info', type => 'string'},
+ {desc => 'Password', type => 'string'}
+ ],
+ return => {desc =>
+ 'Temporary authentication key on success, Event on error'}
+ }
+);
+
+sub authenticate_ldap {
+ my ($self, $client, $auth, $username, $password) = @_;
+
+ return OpenILS::Event->new('BAD_PARAMS')
+ unless $auth && $username && $password;
+
+ # Secondary auth checks require an authenticated staff account.
+ my $e = new_editor(authtoken => $auth); # no xact!
+ return $e->event unless $e->checkauth;
+ return $e->event unless $e->allowed('STAFF_LOGIN');
+
+ if ($username !~ /@/) {
+ # A bare username requires a domain.
+
+ my $ldap_domain = $U->ou_ancestor_setting_value(
+ $e->requestor->ws_ou, 'circ.secondary_auth.ldap.domain', $e);
+
+ return OpenILS::Event->new('LDAP_CONNECTION_ERROR')
+ unless $ldap_domain;
+
+ $username = "$username\@$ldap_domain";
+ }
+
+ $logger->info("LDAP auth request called for $username");
+
+ my $testmode = $U->ou_ancestor_setting_value(
+ $e->requestor->ws_ou, 'circ.secondary_auth.ldap.testmode', $e);
+
+ my $ldap_resp;
+ if ($testmode) {
+ $logger->info("LDAP auth skipping check in testmode");
+ $ldap_resp = {
+ staff_name =>'Test Mode Name',
+ staff_email => $username
+ };
+
+ } else {
+ $ldap_resp = check_ldap_auth($e, $username, $password);
+ return $ldap_resp->{evt} if $ldap_resp->{evt};
+ }
+
+ my $key = md5_hex($$.rand().time);
+
+ OpenSRF::Utils::Cache->new('global')->put_cache(
+ $ldap_key_prefix.$key, {
+ username => $username,
+ staff_name => $ldap_resp->{staff_name},
+ staff_email => $ldap_resp->{staff_email}
+ },
+ $ldap_key_timeout
+ );
+
+ return $key;
+}
+
+sub check_ldap_auth {
+ my ($e, $username, $password) = @_;
+
+ my $ldap_server = $U->ou_ancestor_setting_value(
+ $e->requestor->ws_ou, 'circ.secondary_auth.ldap.server', $e);
+
+ return {evt => OpenILS::Event->new('LDAP_CONNECTION_ERROR')} unless $ldap_server;
+
+ my $connection = Net::LDAP->new($ldap_server, timeout => $ldap_timeout);
+
+ if (!$connection) {
+ $logger->error("Cannot connect to LDAP server $ldap_server : $!");
+ return {evt => OpenILS::Event->new('LDAP_CONNECTION_ERROR')};
+ }
+
+ my $message = $connection->bind($username, password => $password);
+
+ if ($message->is_error) {
+
+ if ($message->code == LDAP_INVALID_CREDENTIALS) {
+ $logger->info("LDAP auth failed for $username");
+ return {evt => OpenILS::Event->new('LDAP_AUTH_FAILED')};
+ }
+
+ # Something else went wrong...
+ my $error = $message->error;
+ $logger->error("LDAP auth for $username returned an error: $error");
+ return {evt => OpenILS::Event->new('LDAP_CONNECTION_ERROR')};
+ }
+
+ # Bind succeeded. Now lookup user info.
+
+ my $users_dn = $U->ou_ancestor_setting_value(
+ $e->requestor->ws_ou, 'circ.secondary_auth.ldap.users_dn', $e);
+
+ # Defaults to ActiveDirectory-style attributes
+ my $users_filter = $U->ou_ancestor_setting_value(
+ $e->requestor->ws_ou, 'circ.secondary_auth.ldap.users_filter', $e) ||
+ '(&(objectCategory=person)(objectClass=user)(userPrincipalName=%s))';
+
+ $users_filter = sprintf($users_filter, $username);
+
+ my $search = $connection->search(
+ base => $users_dn, scope => 'sub', filter => $users_filter);
+
+ my $resp = {};
+ if ($search->count == 0) {
+ $logger->error("LDAP name lookup returned 0 results.");
+ } else {
+ my $entry = $search->entry(0);
+ $resp->{staff_name} = $entry->get_value('cn');
+ $resp->{staff_email} = $entry->get_value('mail');
+ }
+
+ unless ($resp->{staff_name} && $resp->{staff_email}) {
+ $logger->error("LDAP name lookup failed. ".
+ "QUERY: DN=$users_dn ; FILTER=$users_filter");
+ return {evt => OpenILS::Event->new('LDAP_CONNECTION_ERROR')};
+ }
+
+ $connection->unbind;
+ $logger->info("LDAP auth succeeded for $username");
+
+ return $resp;
+}
+
+
+# Called from the payment create API.
+# Caller is responsible for commits and rollbacks
+# Newly created refundable_payment.id's are pushed into $respond_payments
+# so the caller can see what we did.
+# Returns undef on success, Event on error.
+sub create_refundable_payment {
+ my ($class, $e, $secondary_auth_key, $payment_id, $options, $respond_payments) = @_;
+ $options ||= {};
+
+ my $ldap_auth = OpenSRF::Utils::Cache->new('global')
+ ->get_cache($ldap_key_prefix.$secondary_auth_key);
+
+ unless ($ldap_auth &&
+ $ldap_auth->{staff_name} &&
+ $ldap_auth->{staff_email}) {
+ $logger->error("Refundable payment attempted with ".
+ "invalid secondary auth key: $secondary_auth_key");
+ return OpenILS::Event->new('LDAP_AUTH_FAILED');
+ }
+
+ my $payment = $e->retrieve_money_payment([
+ $payment_id, {
+ flesh => 2,
+ flesh_fields => {
+ mp => ['xact'],
+ mbt => ['summary']
+ }
+ }
+ ]) or return $e->die_event;
+
+ # refundable payments may only be applied to circulations.
+ my $circ = $e->retrieve_action_circulation([
+ $payment->xact->id, {
+ flesh => 2,
+ flesh_fields => {
+ circ => ['usr', 'target_copy'],
+ au => [qw/card billing_address mailing_address/]
+ }
+ }
+ ]) or return $e->die_event;
+
+ my $usr = $circ->usr;
+ my $addr = $usr->mailing_address || $usr->billing_address;
+
+ my $mrx = $e->search_money_refundable_xact({xact => $payment->xact->id})->[0];
+
+ if (!$mrx) {
+ # Create the refundable transaction before linking the payment.
+
+ $mrx = Fieldmapper::money::refundable_xact->new;
+ $mrx->xact($payment->xact->id);
+ $mrx->usr_first_name($usr->first_given_name);
+ $mrx->usr_middle_name($usr->second_given_name);
+ $mrx->usr_family_name($usr->family_name);
+ $mrx->usr_barcode($usr->card->barcode);
+ $mrx->usr_street1($addr->street1);
+ $mrx->usr_street2($addr->street2);
+ $mrx->usr_city($addr->city);
+ $mrx->usr_state($addr->state);
+ $mrx->usr_post_code($addr->post_code);
+ $mrx->item_price($U->get_copy_price($e, $circ->target_copy));
+
+ $e->create_money_refundable_xact($mrx) or return $e->die_event;
+ }
+
+ my $accepting_usr;
+ my $ctx_org_id;
+
+ my $desk_payment = $e->retrieve_money_desk_payment($payment->id);
+
+ # credit card payments are "desk payments" but online
+ # card payments will not have a cash_drawer value.
+ if ($desk_payment && $desk_payment->cash_drawer) {
+ $ctx_org_id =
+ $e->retrieve_actor_workstation($desk_payment->cash_drawer)->owning_lib;
+ $accepting_usr = $e->retrieve_actor_user($desk_payment->accepting_usr);
+
+ } else {
+ $ctx_org_id = $circ->circ_lib;
+ $accepting_usr = $usr;
+ }
+
+ # Perm check not needed since this is called from payment create API.
+ # return $e->die_event unless $e->allowed('CREATE_PAYMENT', $ctx_org_id);
+
+ my $mrp = Fieldmapper::money::refundable_payment->new;
+ $mrp->refundable_xact($mrx->id);
+ $mrp->payment($payment->id);
+ $mrp->payment_ou($ctx_org_id);
+
+ $mrp->final_payment('f') if $payment->xact->summary->balance_owed != 0;
+
+ $mrp->staff_name($ldap_auth->{staff_name});
+ $mrp->staff_email($ldap_auth->{staff_email});
+
+ $e->create_money_refundable_payment($mrp) or return $e->die_event;
+
+ push(@$respond_payments, $mrp->id) if $respond_payments;
+
+ return undef;
+}
+
+__PACKAGE__->register_method(
+ method => 'update_refundable_xact',
+ api_name => 'open-ils.circ.refundable_xact.update',
+ signature => {
+ desc => q/Modify a money.refundable_xact'/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Transaction ID', type => 'number'},
+ {desc => 'Arguments', type => 'hash'}
+ ],
+ return => {desc => '1 on success, 0 on no-op, Event on error'}
+ }
+);
+
+sub update_refundable_xact {
+ my ($self, $client, $auth, $mrx_id, $args) = @_;
+ return 0 unless ref $args;
+
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+
+ my $mrx = $e->retrieve_money_refundable_xact($mrx_id)
+ or return $e->die_event;
+
+ for my $f (qw/refund_amount notes/) {
+ $mrx->$f($args->{$f}) if defined $args->{$f};
+ }
+
+ if ($args->{refund}) {
+ $mrx->rejected('f');
+ $mrx->action_date('now');
+ $mrx->action_by($e->requestor->id);
+
+ } elsif ($args->{reject}) {
+ $mrx->rejected('t');
+ $mrx->clear_refund_amount;
+ $mrx->action_date('now');
+ $mrx->action_by($e->requestor->id);
+
+ } elsif ($args->{clear_action}) {
+ $mrx->clear_action_date;
+ $mrx->clear_action_by;
+ $mrx->rejected('f');
+ }
+
+ $e->update_money_refundable_xact($mrx) or return $e->die_event;
+
+ if ($args->{update_payments}) {
+ for my $pay (@{$args->{update_payments}}) {
+ my $mrp = $e->retrieve_money_refundable_payment($pay->{id})
+ or return $e->die_event;
+
+ $mrp->refunded_via($pay->{refunded_via})
+ if defined $pay->{refunded_via};
+
+ $e->update_money_refundable_payment($mrp)
+ or return $e->die_event;
+ }
+ }
+
+ $e->commit;
+ return 1;
+}
+
+__PACKAGE__->register_method(
+ method => 'generate_refundable_payment_receipt',
+ api_name => 'open-ils.circ.refundable_payment.receipt.html',
+ signature => {
+ desc => q/Generate a printable HTML refundable payment receipt/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Refundable Payment ID', type => 'number'}
+ ],
+ return => {
+ desc => 'A/T event with fleshed outputs on success, event on error'
+ }
+ }
+);
+
+__PACKAGE__->register_method(
+ method => 'generate_refundable_payment_receipt',
+ api_name => 'open-ils.circ.refundable_payment.receipt.html',
+ signature => {
+ desc => q/Generate a printable HTML refundable payment receipt/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Refundable Payment ID', type => 'number'}
+ ],
+ return => {
+ desc => 'A/T event with fleshed outputs on success, event on error'
+ }
+ }
+);
+
+__PACKAGE__->register_method(
+ method => 'generate_refundable_payment_receipt',
+ api_name => 'open-ils.circ.refundable_payment.receipt.email',
+ signature => {
+ desc => q/Generate an email refundable payment receipt/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Refundable Payment ID', type => 'number'}
+ ],
+ return => {
+ desc => 'Undef on success, event on error'
+ }
+ }
+);
+
+sub generate_refundable_payment_receipt {
+ my ($self, $client, $auth, $mrp_id) = @_;
+
+ my $e = new_editor(authtoken => $auth);
+ return $e->die_event unless $e->checkauth;
+
+ my $mrps= $e->retrieve_money_refundable_payment_summary([
+ $mrp_id, {flesh => 1, flesh_fields => {mrps => ['refundable_xact']}}
+ ]) or return $e->event;
+
+ # ->usr may be undef when the transaction in question has been purged.
+ # Patrons do not need to print receipts for purged transactions.
+ if ($mrps->refundable_xact->usr &&
+ $mrps->refundable_xact->usr == $e->requestor->id) {
+
+ # Patrons are allowed to print receipts for their own payments.
+ # Nothing to verify here.
+
+ } else {
+ return $e->event unless
+ $e->allowed('CREATE_PAYMENT', $mrps->payment_ou);
+ }
+
+ if ($self->api_name =~ /html/) {
+
+ return $U->fire_object_event(
+ undef, 'format.mrps.html', $mrps, $mrps->payment_ou);
+
+ } else {
+
+ $U->create_events_for_hook(
+ 'format.mrps.email', $mrps, $mrps->payment_ou, undef, undef, 1);
+ return undef;
+ }
+}
+
+1;
+
+
return $U->create_uuid_string;
},
+ # JBAS-1306
+ # Lazy initials generator. Just chops name by spaces, uses the first
+ # letter of each part, separated by a '.'
+ name_to_initials => sub {
+ my $name = shift;
+ return '' unless $name;
+ my @parts = split(/ /, $name);
+ my $initials = join('.', map { substr($_, 0, 1) } @parts) . '.';
+ return $initials;
+ },
+
# escapes quotes in csv string values
escape_csv => sub {
my $string = shift;
'open-ils.actor.user.payments.retrieve.atomic',
$e->authtoken, $e->requestor->id, $args);
+ for my $pay (@{$self->ctx->{payments}}) {
+ $pay->{refundable_payment} =
+ $e->search_money_refundable_payment(
+ {payment => $pay->{mp}->id})->[0];
+ }
+
+
return Apache2::Const::OK;
}
sub load_myopac_receipt_print {
my $self = shift;
- $self->ctx->{printable_receipt} = $U->simplereq(
- "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
- $self->editor->authtoken, [$self->cgi->param("payment")]
- );
+ my $pay_id = $self->cgi->param("payment");
+ my $receipt = '';
+
+ # KCLS JBAS-1306
+ # Refundable payments use a different receipt
+ my $rf_pay =
+ $self->editor->search_money_refundable_payment(
+ {payment => $pay_id})->[0];
+
+ if ($rf_pay) { # Refundable receipt
+
+ $receipt = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.refundable_payment.receipt.html",
+ $self->editor->authtoken, $rf_pay->id
+ );
+
+ } else { # Non-refunable receipt
+
+ $receipt = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.money.payment_receipt.print",
+ $self->editor->authtoken, [$pay_id]
+ );
+ }
+ $self->ctx->{printable_receipt} = $receipt;
return Apache2::Const::OK;
}
sub load_myopac_receipt_email {
my $self = shift;
- # The following ML method doesn't actually check whether the user in
- # question has an email address, so we do.
- if ($self->ctx->{user}->email) {
- $self->ctx->{email_receipt_result} = $U->simplereq(
- "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
- $self->editor->authtoken, [$self->cgi->param("payment")]
- );
- } else {
+ if (!$self->ctx->{user}->email) {
$self->ctx->{email_receipt_result} =
new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
+ return Apache2::Const::OK;
+ }
+
+ my $pay_id = $self->cgi->param("payment");
+ my $result;
+
+ # KCLS JBAS-1306
+ # Refundable payments use a different receipt
+ my $rf_pay =
+ $self->editor->search_money_refundable_payment(
+ {payment => $pay_id})->[0];
+
+
+ if ($rf_pay) { # Refundable receipt
+
+ $result = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.refundable_payment.receipt.email",
+ $self->editor->authtoken, $rf_pay->id
+ );
+
+ } else {
+ $result = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.money.payment_receipt.email",
+ $self->editor->authtoken, [$pay_id]
+ );
}
+ $self->ctx->{email_receipt_result} = $result;
return Apache2::Const::OK;
}
--- /dev/null
+[%
+ WRAPPER "staff/base.tt2";
+ ctx.page_title = l("Refundable Payment Transactions");
+ ctx.page_app = "egRefundApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/refunds/app.js"></script>
+
+<style>
+ .money-input {
+ width: 5em;
+ height: 75%;
+ text-align: right;
+ }
+ .detail-header {
+ border-bottom: 1px dashed #BEBEBE;
+ }
+ .selected-state { text-decoration: underline }
+ .invalid-amount { color:red }
+ #all-billings-list {
+ border-right: 1px solid #888;
+ }
+ .full-details-container:nth-child(odd) {
+ background-color: rgb(248, 248, 248)
+ }
+</style>
+[% END %]
+
+<div class="container-fluid" style="text-align:center">
+ <div class="alert alert-info alert-less-pad strong-text-2">
+ <span>[% l('Refundable Transactions') %]</span>
+ </div>
+</div>
+
+<div ng-view></div>
+
+[% END %]
--- /dev/null
+<!-- TODO
+ * editing refunded xacts / modifying notes
+ ** Apply & Cancel buttons
+ * test CC payments / refunded_via
+ * rejecting
+-->
+
+<div class="row">
+ <div class="col-md-12">
+ <span>
+ <button class="btn btn-success" ng-click="return_to_list()">
+ <span class="glyphicon glyphicon-chevron-left"></span>
+ Return To List
+ </button>
+ </span>
+ <span class='pad-left-more'>
+ <button class="btn btn-info" ng-disabled="invalid_refund_amount()"
+ ng-click="apply_updates()">
+ <span class="glyphicon glyphicon-pencil"></span>
+ Apply Updates
+ </button>
+ </span>
+ <span class='pad-left-more'>Mark As:</span>
+ <span class='pad-left'>
+ <input type='radio' ng-model="xact_state" value="pending"/>
+ <span class="pad-left-min"
+ ng-class="{'selected-state':xact_state == 'pending'}">Pending</span>
+ </span>
+ <span class='pad-left'>
+ <input type='radio' ng-model="xact_state"
+ ng-disabled="mrxs.action_date() && xact_state == 'rejected'" value="refunded"/>
+ <span class="pad-left-min"
+ ng-class="{'selected-state':xact_state == 'refunded'}">Refunded</span>
+ </span>
+ <span class='pad-left'>
+ <input type='radio' ng-model="xact_state" value="rejected"/>
+ <span class="pad-left-min"
+ ng-class="{'selected-state':xact_state == 'rejected'}">Rejected</span>
+ </span>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <h3 class="detail-header">Transaction Summary</h3>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-6">
+ <div class="row">
+ <div class="col-md-6">Transaction ID</div>
+ <div class="col-md-6">{{mrxs.xact().id()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Amount Paid</div>
+ <div class="col-md-6">{{mrxs.refundable_paid() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">B.O. Refund Amount</div>
+ <div class="col-md-6 strong-text" ng-if="mrxs.rejected() == 't'">
+ Rejected
+ </div>
+ <div class="col-md-6 strong-text"
+ ng-if="!editing && mrxs.rejected() != 't'">
+ {{mrxs.refund_amount() | currency}}
+ </div>
+ <div class="col-md-6" ng-if="editing">
+ <input type="text" class="money-input"
+ ng-class="{'invalid-amount':invalid_refund_amount()}"
+ select-me="select_refund_amt"
+ ng-model="mrxs.refund_amount"
+ ng-model-options="{getterSetter: true}"
+ />
+ </div>
+ </div>
+ <div class="row" ng-if="!editing">
+ <div class="col-md-6">B.O. Action Date</div>
+ <div class="col-md-6">{{mrxs.action_date() | date:'short'}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Copy Barcode</div>
+ <div class="col-md-6">{{mrxs.copy_barcode()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Title</div>
+ <div class="col-md-6">{{mrxs.title()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Patron ID</div>
+ <div class="col-md-6">{{mrxs.usr()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Patron Name</div>
+ <div class="col-md-6">
+ {{mrxs.usr_first_name()}}
+ {{mrxs.usr_middle_name()}}
+ {{mrxs.usr_family_name()}}
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-6">Patron Address</div>
+ <div class="col-md-6">
+ {{mrxs.usr_street1()}} {{mrxs.usr_street2()}}
+ {{mrxs.usr_city()}}, {{mrxs.usr_state()}} {{mrxs.usr_post_code()}}
+ </div>
+ </div>
+
+ </div>
+ <div class="col-md-5">
+ <textarea class="form-control" rows="7" placeholder="Notes..."
+ ng-model="mrxs.notes" ng-model-options="{getterSetter: true}">
+ </textarea>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <h3 class="detail-header">Payments</div>
+ </div>
+</div>
+
+<div ng-repeat="mrps in mrxs.refundable_payments()">
+ <div class="row">
+ <div class="col-md-3">Receipt Code</div>
+ <div class="col-md-3">{{mrps.receipt_code()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Amount</div>
+ <div class="col-md-3">{{mrps.amount() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Payment Date</div>
+ <div class="col-md-3">{{mrps.payment_time() | date:'short'}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Payment Type</div>
+ <div class="col-md-3">
+ <span ng-switch on="mrps.payment_type()">
+ <span ng-switch-when="cash_payment">Cash</span>
+ <span ng-switch-when="check_payment">Check</span>
+ <span ng-switch-when="credit_card_payment">Credit Card</span>
+ <span ng-switch-default>{{mrps.payment_type()}}</span>
+ </span>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Final Payment</div>
+ <div class="col-md-3">
+ <span class="label label-success"
+ ng-if="mrps.final_payment() == 't'">Yes</span>
+ <span ng-if="mrps.final_payment() != 't'">No</span>
+ </div>
+ </div>
+ <div class="row" ng-if="mrps.payment_type() == 'credit_card_payment'">
+ <div class="col-md-3">Refunded Via</div>
+ <div class="col-md-6">
+ <span>
+ <input type='radio' ng-model="mrps.refunded_via"
+ ng-model-options="{getterSetter: true}" value="check"/>
+ <span class="pad-left-min">Check</span>
+ </span>
+ <span class='pad-left'>
+ <input type='radio' ng-model="mrps.refunded_via"
+ ng-model-options="{getterSetter: true}" value="credit_card"/>
+ <span class="pad-left-min">Credit Card</span>
+ </span>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Staff Name</div>
+ <div class="col-md-3">{{mrps.staff_name()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-3">Staff Email</div>
+ <div class="col-md-3">{{mrps.staff_email()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-md-6"><hr/></div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+ <h3 class="detail-header">All Transaction Billings and Payments</div>
+ </div>
+</div>
+<div class="row">
+ <div class="col-md-7" id="all-billings-list">
+ <h4>All Billings</h4>
+ <div class="row" style="font-weight:bold">
+ <div class="col-md-2">Date</div>
+ <div class="col-md-1">ID#</div>
+ <div class="col-md-2">Amount</div>
+ <div class="col-md-3">Type</div>
+ <div class="col-md-2">Voided</div>
+ <div class="col-md-2">Void Date</div>
+ </div>
+ <div class="full-details-container" ng-repeat="bill in mrxs.xact().billings()">
+ <div class="row">
+ <div class="col-md-2">{{bill.billing_ts() | date:'shortDate'}}</div>
+ <div class="col-md-1">{{bill.id()}}</div>
+ <div class="col-md-2">{{bill.amount() | currency}}</div>
+ <div class="col-md-3">{{bill.btype().name()}}</div>
+ <div class="col-md-2">
+ <div ng-if="bill.voided() == 't'">
+ <span class="text-danger">Yes</span>
+ </div>
+ </div>
+ <div class="col-md-2">
+ {{bill.void_time() | date:'shortDate'}}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-5">
+ <h4>All Payments</h4>
+ <div class="row" style="font-weight:bold">
+ <div class="col-md-3">Date</div>
+ <div class="col-md-2">ID#</div>
+ <div class="col-md-2">Amount</div>
+ <div class="col-md-3">Type</div>
+ <div class="col-md-2">Voided</div>
+ </div>
+ <div class="full-details-container" ng-repeat="pay in mrxs.xact().payments()">
+ <div class="row">
+ <div class="col-md-3">{{pay.payment_ts() | date:'shortDate'}}</div>
+ <div class="col-md-2">{{pay.id()}}</div>
+ <div class="col-md-2">{{pay.amount() | currency}}</div>
+ <div class="col-md-3">
+ <span ng-switch on="pay.payment_type()">
+ <span ng-switch-when="cash_payment">Cash</span>
+ <span ng-switch-when="check_payment">Check</span>
+ <span ng-switch-when="credit_card_payment">Credit Card</span>
+ <span ng-switch-default>{{pay.payment_type()}}</span>
+ </span>
+ </div>
+ <div class="col-md-2">
+ <div ng-if="pay.voided() == 't'">
+ <span class="text-danger">Yes</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+
--- /dev/null
+<div class="row pad-vert">
+ <div class="col-lg-4">
+ <div class="input-group">
+ <div class="input-group-btn" uib-dropdown>
+ <button type="button" class="btn btn-default" uib-dropdown-toggle>
+ <span ng-switch on="ctx.search_param">
+ <span ng-switch-when="receipt_code">Receipt Code</span>
+ <span ng-switch-when="usr_name">Patron Last, First</span>
+ <span ng-switch-when="usr_barcode">Patron Barcode</span>
+ <span ng-switch-when="usr">Patron ID</span>
+ <span ng-switch-default>Search By</span>
+ </span>
+ <span class="caret"></span>
+ </button>
+ <ul uib-dropdown-menu>
+ <li><a href ng-click="ctx.search_param='receipt_code'">Receipt Code</a></li>
+ <li><a href ng-click="ctx.search_param='usr_name'">Patron Last, First</a></li>
+ <li><a href ng-click="ctx.search_param='usr_barcode'">Patron Barcode</a></li>
+ <li><a href ng-click="ctx.search_param='usr'">Patron ID</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ <input type="text" class="form-control" ng-model="ctx.search_query"
+ placeholder="Search for..."/>
+ </div><!-- /input-group -->
+ </div>
+ <div class="col-md-1">
+ <button ng-click="ctx.perform_search()" class="btn btn-default">Search</button>
+ </div>
+ <div class="col-md-6">
+ <!-- bootstrap checkboxes don't want to sit next to each other
+ unless they're in a form wrapper, etc. Just use spans. -->
+ <div style='margin-top:10px'>
+ <span class='pad-right'>
+ <input type='checkbox' ng-model="ctx.limit_to_refundable"
+ ng-change='ctx.perform_search()'
+ ng-disabled="ctx.search_param=='receipt_code'"/>
+ <span class='pad-all-min'>Limit to Refund Pending</span>
+ </span>
+ <span>
+ <input type='checkbox' ng-model="ctx.limit_to_1year"
+ ng-change='ctx.perform_search()'
+ ng-disabled="ctx.search_param=='receipt_code'"/>
+ <span class='pad-all-min'>Limit to Last 12 Months</span>
+ </span>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-12">
+
+ <eg-grid
+ idl-class="mrxs"
+ items-provider="gridDataProvider"
+ grid-controls="gridControls"
+ persist-key="circ.refunds.list">
+
+ <eg-grid-field path="id" required hidden></eg-grid-field>
+ <eg-grid-field path="xact" required hidden></eg-grid-field>
+ <eg-grid-field name="last_payment" label="Last Payment" nonsortable>
+ {{item._last_payment.receipt_code()}} ({{item.num_refundable_payments()}})
+ </eg-grid-field>
+ <eg-grid-field name="last_payment" label="Last Payment Date" nonsortable>
+ {{item._last_payment.payment_time() | date:'short' }}
+ </eg-grid-field>
+ <eg-grid-field path="refundable_paid" label="Amount Paid"></eg-grid-field>
+ <eg-grid-field path="usr" label="Patron ID"></eg-grid-field>
+ <eg-grid-field path="copy_barcode"></eg-grid-field>
+ <eg-grid-field path="usr_first_name" hidden></eg-grid-field>
+ <eg-grid-field path="usr_middle_name" hidden></eg-grid-field>
+ <eg-grid-field path="usr_family_name"></eg-grid-field>
+ <eg-grid-field path="usr_street1" flex="3" label="Patron Street Addr."></eg-grid-field>
+ <eg-grid-field path="refund_amount" label="B.O. Refund Amount"></eg-grid-field>
+ <eg-grid-field path="action_date" label="B.O. Action Date"></eg-grid-field>
+ <eg-grid-field path="title" hidden></eg-grid-field>
+ <eg-grid-field path="usr_street2" hidden></eg-grid-field>
+ <eg-grid-field path="usr_city" hidden></eg-grid-field>
+ <eg-grid-field path="usr_state" hidden></eg-grid-field>
+ <eg-grid-field path="usr_post_code" hidden></eg-grid-field>
+ <eg-grid-field path="rejected" hidden></eg-grid-field>
+ <eg-grid-field path="notes" hidden></eg-grid-field>
+ <eg-grid-field path="num_refundable_payments" label="Payments" hidden required></eg-grid-field>
+ </div>
+</div>
+
.pad-horiz {padding : 0px 10px 0px 10px; }
.pad-vert {padding : 20px 0px 10px 0px;}
+.pad-vert-min {padding : 5px 0px 5px 0px;}
+.pad-vert-min-2 {padding : 2px 0px 2px 0px;}
.pad-left {padding-left: 10px;}
+.pad-left-more {padding-left: 20px;}
+.pad-left-min {padding-left: 5px;}
.pad-right {padding-right: 10px;}
.pad-right-min {padding-right: 5px;}
.pad-all-min {padding : 5px; }
--- /dev/null
+angular.module('egRefundApp',
+ ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+ $locationProvider.html5Mode(true);
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+ var resolver = {delay :
+ ['egStartup', function(egStartup) {return egStartup.go()}]}
+
+ $routeProvider.when('/circ/refunds/list', {
+ templateUrl: './circ/refunds/t_list',
+ controller: 'RefundListCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.when('/circ/refunds/view/:id', {
+ templateUrl: './circ/refunds/t_detail',
+ controller: 'RefundDetailCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.otherwise({redirectTo : '/circ/refunds/list'});
+})
+
+.factory('mrxSvc',
+ ['$q','egCore',
+function($q , egCore) {
+
+ var service = {
+
+ // store filters in the service so they persist across
+ // list and details pages.
+ filters : {
+ limit_to_refundable : true,
+ limit_to_1year : true
+ },
+
+ // sort payments newest to oldest and also set the
+ // newest as the _last_payment attribute on the mrxs.
+ sort_payments : function(mrxs) {
+ var payments = mrxs.refundable_payments().sort(
+ function(a, b) {
+ return a.payment_time() > b.payment_time() ? -1 : 1
+ }
+ );
+ mrxs.refundable_payments(payments);
+ mrxs._last_payment = payments[0];
+ }
+ };
+
+ return service;
+}])
+
+
+/**
+ */
+.controller('RefundListCtrl',
+ ['$scope','$q','$window','$location','egCore','egGridDataProvider','mrxSvc',
+function($scope , $q , $window , $location , egCore , egGridDataProvider , mrxSvc) {
+
+ $scope.ctx = {
+ limit_to_refundable : mrxSvc.filters.limit_to_refundable,
+ limit_to_1year : mrxSvc.filters.limit_to_1year
+ };
+
+ $scope.gridControls = {
+ activateItem : function(item) {
+ // dbl-click goes to transaction detail view
+ $location.path('/circ/refunds/view/' + item.id());
+ }
+ };
+
+ var provider
+ = $scope.gridDataProvider
+ = egGridDataProvider.instance({});
+
+ $scope.ctx.perform_search = function() {
+ // copy UI filters back into service
+ mrxSvc.filters.limit_to_refundable = $scope.ctx.limit_to_refundable;
+ mrxSvc.filters.limit_to_1year = $scope.ctx.limit_to_1year;
+
+ provider.refresh();
+ }
+
+ function compile_query() {
+ var query = {};
+
+ if ($scope.ctx.limit_to_1year) {
+ var last_year = new Date();
+ last_year.setFullYear(last_year.getFullYear() - 1);
+ query['-or'] = [
+ {xact_finish : {'>=' : last_year.toISOString()}},
+ {xact_finish : null}
+ ]
+ }
+
+ if ($scope.ctx.limit_to_refundable) {
+ query.action_date = null;
+ }
+
+ if (!($scope.ctx.search_param && $scope.ctx.search_query)) {
+ query.id = {'<>' : null};
+
+ } else if ($scope.ctx.search_param == 'usr_name') {
+
+ var parts = $scope.ctx.search_query.split(/,/);
+ query.usr_family_name = {'~*' : parts[0].trim()};
+ if (parts[1]) query.usr_first_name = {'~*' : parts[1].trim()};
+
+ } else if ($scope.ctx.search_param == 'receipt_code') {
+ return receipt_code_query();
+
+ } else {
+ query[$scope.ctx.search_param] = $scope.ctx.search_query;
+ }
+
+ return $q.when(query);
+ }
+
+ function receipt_code_query() {
+ // Querying the receipt code means query payments instead
+ // of transactions.
+
+ return egCore.pcrud.search('mrps',
+ {receipt_code : $scope.ctx.search_query}
+ ).then(function(payment) {
+ if (payment) return {id : payment.refundable_xact()};
+
+ // if no payments found, return a no-op query.
+ return {id : null};
+ });
+ }
+
+ function compile_sort() {
+ // turn the grid sort data into a pcrud sort string.
+ var order_by = '';
+ angular.forEach(provider.sort, function(col) {
+ if (order_by) order_by += ',';
+ if (angular.isObject(col)) {
+ var name = Object.keys(col)[0];
+ order_by += name + ' ' + col[name];
+ } else {
+ order_by += col;
+ }
+ });
+
+ return order_by ? {mrxs : order_by} : {};
+ }
+
+ provider.get = function(offset, count) {
+
+ var deferred = $q.defer();
+
+ compile_query().then(function(query) {
+
+ return egCore.pcrud.search('mrxs', query,
+ { limit : count,
+ offset : offset,
+ flesh : 1,
+ flesh_fields : {mrxs : ['refundable_payments']},
+ order_by : compile_sort()
+ }
+ ).then(
+ deferred.resolve,
+ deferred.reject,
+ function(mrxs) {
+ mrxSvc.sort_payments(mrxs);
+ deferred.notify(mrxs);
+ }
+ );
+ });
+
+ return deferred.promise;
+ }
+
+}])
+
+.controller('RefundDetailCtrl',
+ ['$scope','$q','$routeParams','$window','$location','egCore','mrxSvc',
+function($scope , $q , $routeParams , $window , $location , egCore , mrxSvc) {
+
+ // TODO: make this call authoritative once we're running on
+ // newer pcrud.js code that supports it. (IIRC 2.12 or later)
+
+ function load_data() {
+ egCore.pcrud.retrieve('mrxs', $routeParams.id, {
+ flesh : 4,
+ flesh_fields : {
+ mrxs : ['refundable_payments', 'xact'],
+ mbt : ['payments', 'billings'],
+ mb : ['btype']
+ },
+ }).then(function(mrxs) {
+ if (!mrxs) return;
+ if (!mrxs.refund_amount()) mrxs.refund_amount('0.00');
+
+ if (mrxs.action_date()) {
+ $scope.editing = false;
+ if (mrxs.rejected() == 't') {
+ $scope.xact_state = 'rejected';
+ } else {
+ $scope.xact_state = 'refunded';
+ }
+ } else {
+ $scope.editing = true;
+ $scope.xact_state = 'pending';
+ }
+
+ mrxs.xact().billings(
+ mrxs.xact().billings().sort(function(a, b) {
+ return a.billing_ts() > b.billing_ts() ? -1 : 1
+ })
+ );
+
+ mrxs.xact().payments(
+ mrxs.xact().payments().sort(function(a, b) {
+ return a.payment_ts() > b.payment_ts() ? -1 : 1
+ })
+ );
+
+ $scope.mrxs = mrxs;
+ mrxSvc.sort_payments(mrxs);
+ $scope.select_refund_amt = true;
+ });
+ }
+
+ $scope.return_to_list = function() {
+ // navigate to the detail view on double-click
+ $location.path('/circ/refunds/list');
+ }
+
+ $scope.invalid_refund_amount = function() {
+ if (!$scope.mrxs) return false;
+
+ var amt = Number($scope.mrxs.refund_amount());
+
+ if (amt < 0 || amt > Number($scope.mrxs.refundable_paid()))
+ return true;
+
+ // $0.00 is not a valid amount when marking as refunded,
+ // but OK otherwise.
+ if ($scope.xact_state == 'refunded') return amt == 0;
+
+ return false;
+ }
+
+ $scope.apply_updates = function() {
+ var args = {
+ notes : $scope.mrxs.notes(),
+ refund_amount : $scope.mrxs.refund_amount()
+ };
+
+ args.clear_action = $scope.xact_state == 'pending';
+ args.refund = $scope.xact_state == 'refunded';
+ args.reject = $scope.xact_state == 'rejected';
+
+ args.update_payments = [];
+ angular.forEach($scope.mrxs.refundable_payments(),
+ function(pay) {
+ if (pay.refunded_via()) {
+ args.update_payments.push(
+ {id : pay.id(), refunded_via : pay.refunded_via()});
+ }
+ }
+ );
+
+ egCore.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.refundable_xact.update',
+ egCore.auth.token(), $scope.mrxs.id(), args
+ ).then(function(result) {
+ var evt = egCore.evt.parse(result);
+ if (evt) {
+ alert(evt);
+ } else {
+ load_data(); // refresh
+ }
+ });
+ }
+
+ load_data();
+}])
+
+
'XUL_PATRON_BILL_DETAILS' : 'oils://remote/xul/server/patron/bill_details.xul',
'XUL_PATRON_BILL_HISTORY' : 'oils://remote/xul/server/patron/bill_history.xul',
'XUL_PATRON_BILL_WIZARD' : 'oils://remote/xul/server/patron/bill_wizard.xul',
+ 'XUL_PATRON_BILL_APPLY_PAYMENT_FORM' : 'oils://remote/xul/server/patron/bill_apply_payment_form.xul',
'XUL_PATRON_DISPLAY' : 'oils://remote/xul/server/patron/display.xul',
'XUL_PATRON_HORIZ_DISPLAY' : 'oils://remote/xul/server/patron/display_horiz.xul',
'XUL_PATRON_EDIT' : 'oils://remote/eg/actor/user/register',
}
}
+/**
+ * Launch secondary authentication dialog.
+ * "Skip" option bypasses the secondary authentication step.
+ * "Cancel" option prevents the payment from proceeding.
+ * "Submit" option requests a secondary authentication token from the
+ * server, which is later used by the payment create API.
+ * If a login failure occurs, the dialog is reopened for additional
+ * authentication attempts.
+ *
+ * Returns null on Cancel
+ * Returns unaltered payment_blob if no applicable (LOST) payments are
+ * being processed OR if the dialog is manually skipped.
+ * Returns payment args (with 2ndry auth data) on successful Submit.
+ */
+function handle_lost_payment_dialog(payment_blob) {
+
+ var lostpaid_xacts = [];
+ var row_ids = g.bill_list.dump_retrieve_ids();
+
+ // Find LOST transactions in the pending payments.
+ payment_blob.payments.forEach(function(xact) {
+ // xact[0] == transaction_id, xact[1] == payment amount
+ row_ids.forEach(function(row_id) {
+ var row_params = g.row_map[row_id];
+ var circ = row_params.row.my.circ;
+ if (!circ) return;
+ if (circ.id() == xact[0]) {
+ if (circ.stop_fines() == 'LOST') {
+ lostpaid_xacts.push(xact[0]);
+ }
+ }
+ });
+ });
+
+ // No lost+paid transcactions to process.
+ if (lostpaid_xacts.length == 0) return payment_blob;
+
+ // If we have any LOST payments, do the secondary auth dance.
+
+ JSAN.use('util.window');
+ while (true) {
+
+ var win = new util.window();
+ var dialog = win.open(
+ urls.XUL_PATRON_BILL_APPLY_PAYMENT_FORM,
+ 'applypayment',
+ 'chrome,resizable,modal'
+ );
+
+ if (dialog.skip_lost_payment) {
+ return payment_blob;
+ } else if (!dialog.process_lost_payment) {
+ // Payment canceled from dialog
+ return null;
+ }
+
+ // Attempt secondary authentication
+
+ var resp = g.network.request(
+ 'open-ils.circ',
+ 'open-ils.circ.staff.secondary_auth.ldap',
+ [ses(), dialog.username, dialog.password]
+ );
+
+ if (resp && typeof resp == 'string') {
+ // Secondary auth succeeded.
+ // Embed the refundable args data in the in-progress payment blob.
+ payment_blob.refundable_args = {
+ secondary_auth_key : resp,
+ transactions : lostpaid_xacts.map(
+ function(x) {
+ return {xact : x};
+ }
+ )
+ };
+
+ return payment_blob;
+ }
+
+ if (resp && resp.textcode) {
+ // If the secondary auth fails for any reason, re-open the
+ // dialog. User may Cancel at any time to break out
+ // of the loop.
+
+ if (resp.textcode == 'LDAP_AUTH_FAILED') {
+ alert('Secondary Authentication Failed');
+ $('apply_payment_password').value = '';
+
+ } else {
+ g.error.standard_unexpected_error_alert(
+ 'Secondary Authentication Error', resp);
+ }
+
+ } else { // unknown failure
+ g.error.standard_unexpected_error_alert(
+ 'Secondary Authentication Error');
+ }
+ }
+}
+
+function apply_payment_skip_form() {
+ this.skip_lost_payment = true;
+ this.close();
+}
+
+function apply_payment_cancel() {
+ this.process_lost_payment = false;
+ this.close();
+}
+
+function apply_payment_submit_form() {
+ this.process_lost_payment = true;
+
+ this.username = $('apply_payment_username').value;
+ this.password = $('apply_payment_password').value;
+
+ // leave the dialog open if required fields are empty.
+ if (!this.username || !this.password) return;
+
+ this.close();
+}
+
function $(id) { return document.getElementById(id); }
function default_focus() {
function apply_payment() {
+
try {
var payment_blob = {};
JSAN.use('util.window');
alert($("patronStrings").getString('staff.patron.bills.apply_payment.nothing_applied'));
return;
}
+
+ payment_blob = handle_lost_payment_dialog(payment_blob);
+ if (payment_blob === null) return; // payment canceled via dialog
+
if ( pay( payment_blob ) ) {
$('payment').value = ''; $('payment').select(); $('payment').focus();
alert('Error logging payment in bill2.js: ' + E);
}
+ if (robj && robj.refundable_payments
+ && robj.refundable_payments.length) {
+ print_refundable_payments_receipt(robj.refundable_payments);
+ }
+
if (typeof robj.ilsevent != 'undefined') {
switch(robj.textcode) {
case 'SUCCESS' : return true; break;
}
}
+function print_refundable_payments_receipt(mrp_ids) {
+ for (var i = 0; i < mrp_ids.length; i++) {
+ mrp_id = mrp_ids[0];
+ var receipt = g.network.request(
+ 'open-ils.circ',
+ 'open-ils.circ.refundable_payment.receipt.html',
+ [ses(), mrp_id]
+ );
+
+ if (!receipt || !receipt.template_output()) {
+ return alert(
+ 'Error creating refundable payment receipt for payment ' + mrp_id);
+ }
+
+ var html = receipt.template_output().data();
+ JSAN.use('util.print'); var print = new util.print('mail');
+ print.simple(html , {
+ no_prompt: false, /*TODO*/
+ content_type: 'text/html'
+ });
+ }
+}
+
function refresh(params) {
try {
if (g.safe_for_refresh) {
--- /dev/null
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Patron Display -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/circ.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/patron_display.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+ <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<window width="550" height="175" oils_persist="width height sizemode"
+ onload="my_init()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+ <!-- BEHAVIOR -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+<?xul-overlay href="/xul/server/patron/bill_summary_overlay.xul"?>
+ <script type="text/javascript">var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};</script>
+ <scripts id="openils_util_scripts"/>
+
+ <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+ <script type="text/javascript" src="/xul/server/patron/bill2.js"/>
+ <script>
+ function my_init() {
+ // Focus the username input on page load
+ setTimeout(
+ function() {
+ document.getElementById('apply_payment_username').focus();
+ }
+ );
+ }
+
+ function check_enter(event) {
+ if (event.keyCode != 13) return;
+ if (!document.getElementById('apply_payment_username').value) return;
+ if (!document.getElementById('apply_payment_password').value) return;
+
+ this.apply_payment_submit_form();
+ }
+
+ </script>
+
+ <messagecatalog id="patronStrings" src="/xul/server/locale/<!--#echo var='locale'-->/patron.properties"/>
+
+ <vbox flex="1" class="my_overflow">
+
+ <groupbox id="bill_payment_form" flex="1" >
+ <caption label="Please enter credentials for tracking payments for LOST items."/>
+ <hbox>
+ <vbox>
+ <label control="apply_payment_username" value="Username"/>
+ <textbox id="apply_payment_username"
+ onkeypress="check_enter(event)" maxwidth="150"/>
+ </vbox>
+ <vbox>
+ <label control="apply_payment_password" value="Password"/>
+ <textbox id="apply_payment_password" type="password"
+ onkeypress="check_enter(event)" maxwidth="150"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+ <hbox>
+ <button label="Submit" accesskey="S"
+ id='apply_lost_payment_submit'
+ oncommand="apply_payment_submit_form()"/>
+ <spacer flex="1"/>
+ <button
+ hidden="true"
+ label="Skip Refund Tracking" accesskey="K"
+ id='apply_lost_payment_skip'
+ oncommand="apply_payment_skip_form()"/>
+ <button
+ label="Cancel Payment" accesskey="C"
+ id='apply_lost_payment_cancel'
+ oncommand="apply_payment_cancel()"/>
+ </hbox>
+ </vbox>
+
+</window>