From 3367464020fdb777f38a3d7d6fba8c1f8f9af4ef Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 3 Jul 2017 17:16:42 -0400 Subject: [PATCH] JBAS-1306 Lost/paid Payment Reciepts & Tracking * refundable payment tracking * rf payment print / email receipts (re-printable) * rf payment staff 2ndry authorization via ldap * rf payment staff view (for B.O.) Signed-off-by: Bill Erickson --- .../templates_kcls/opac/biblio/main_payments.tt2 | 3 +- KCLS/sql/schema/deploy/lost-paid-receipts-data.sql | 306 +++++++++++++++ KCLS/sql/schema/deploy/lost-paid-receipts.sql | 143 +++++++ KCLS/sql/schema/revert/lost-paid-receipts-data.sql | 53 +++ KCLS/sql/schema/revert/lost-paid-receipts.sql | 12 + KCLS/sql/schema/sqitch.plan | 2 + KCLS/sql/schema/verify/lost-paid-receipts-data.sql | 7 + KCLS/sql/schema/verify/lost-paid-receipts.sql | 7 + Open-ILS/examples/fm_IDL.xml | 144 ++++++- .../src/perlmods/lib/OpenILS/Application/Circ.pm | 1 + .../perlmods/lib/OpenILS/Application/Circ/Money.pm | 47 ++- .../OpenILS/Application/Circ/RefundablePayment.pm | 426 +++++++++++++++++++++ .../lib/OpenILS/Application/Trigger/Reactor.pm | 11 + .../lib/OpenILS/WWW/EGCatLoader/Account.pm | 74 +++- .../src/templates/staff/circ/refunds/index.tt2 | 40 ++ .../src/templates/staff/circ/refunds/t_detail.tt2 | 248 ++++++++++++ .../src/templates/staff/circ/refunds/t_list.tt2 | 85 ++++ Open-ILS/src/templates/staff/css/style.css.tt2 | 4 + .../web/js/ui/default/staff/circ/refunds/app.js | 285 ++++++++++++++ .../staff_client/chrome/content/main/constants.js | 1 + Open-ILS/xul/staff_client/server/patron/bill2.js | 155 ++++++++ .../server/patron/bill_apply_payment_form.xul | 88 +++++ 22 files changed, 2124 insertions(+), 18 deletions(-) create mode 100644 KCLS/sql/schema/deploy/lost-paid-receipts-data.sql create mode 100644 KCLS/sql/schema/deploy/lost-paid-receipts.sql create mode 100644 KCLS/sql/schema/revert/lost-paid-receipts-data.sql create mode 100644 KCLS/sql/schema/revert/lost-paid-receipts.sql create mode 100644 KCLS/sql/schema/verify/lost-paid-receipts-data.sql create mode 100644 KCLS/sql/schema/verify/lost-paid-receipts.sql create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/RefundablePayment.pm create mode 100644 Open-ILS/src/templates/staff/circ/refunds/index.tt2 create mode 100644 Open-ILS/src/templates/staff/circ/refunds/t_detail.tt2 create mode 100644 Open-ILS/src/templates/staff/circ/refunds/t_list.tt2 create mode 100644 Open-ILS/web/js/ui/default/staff/circ/refunds/app.js create mode 100644 Open-ILS/xul/staff_client/server/patron/bill_apply_payment_form.xul diff --git a/KCLS/openils/var/templates_kcls/opac/biblio/main_payments.tt2 b/KCLS/openils/var/templates_kcls/opac/biblio/main_payments.tt2 index 50783825bb..dacc99f035 100644 --- a/KCLS/openils/var/templates_kcls/opac/biblio/main_payments.tt2 +++ b/KCLS/openils/var/templates_kcls/opac/biblio/main_payments.tt2 @@ -47,7 +47,8 @@ %] [% money(payment.mp.amount) %] - [% IF payment.mp.payment_type == 'credit_card_payment' %] + [% IF payment.mp.payment_type == 'credit_card_payment' + OR payment.refundable_payment %]
diff --git a/KCLS/sql/schema/deploy/lost-paid-receipts-data.sql b/KCLS/sql/schema/deploy/lost-paid-receipts-data.sql new file mode 100644 index 0000000000..1fdda60ee7 --- /dev/null +++ b/KCLS/sql/schema/deploy/lost-paid-receipts-data.sql @@ -0,0 +1,306 @@ +-- 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') -%] +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Receipt #[% mrps.receipt_code %]
Patron ID[% mrps.refundable_xact.usr %]
Patron Name + [% mrps.refundable_xact.usr_first_name %] + [% mrps.refundable_xact.usr_middle_name %] + [% mrps.refundable_xact.usr_family_name %] +
Patron Address + [% 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 %] +
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 + + 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.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 + +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; diff --git a/KCLS/sql/schema/deploy/lost-paid-receipts.sql b/KCLS/sql/schema/deploy/lost-paid-receipts.sql new file mode 100644 index 0000000000..96df20ae17 --- /dev/null +++ b/KCLS/sql/schema/deploy/lost-paid-receipts.sql @@ -0,0 +1,143 @@ + +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; + diff --git a/KCLS/sql/schema/revert/lost-paid-receipts-data.sql b/KCLS/sql/schema/revert/lost-paid-receipts-data.sql new file mode 100644 index 0000000000..e8e17b3f3f --- /dev/null +++ b/KCLS/sql/schema/revert/lost-paid-receipts-data.sql @@ -0,0 +1,53 @@ +-- 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; + diff --git a/KCLS/sql/schema/revert/lost-paid-receipts.sql b/KCLS/sql/schema/revert/lost-paid-receipts.sql new file mode 100644 index 0000000000..ccda1f37c7 --- /dev/null +++ b/KCLS/sql/schema/revert/lost-paid-receipts.sql @@ -0,0 +1,12 @@ +-- 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; diff --git a/KCLS/sql/schema/sqitch.plan b/KCLS/sql/schema/sqitch.plan index 265fd6c5e1..aa87fe5f49 100644 --- a/KCLS/sql/schema/sqitch.plan +++ b/KCLS/sql/schema/sqitch.plan @@ -73,6 +73,8 @@ ecard-data [2.10-to-2.12-upgrade] 2018-01-03T21:55:03Z Bill Erickson,,, # Remove gender field/data ecard-notice-validator [remove-gender] 2018-07-26T14:33:57Z Bill Erickson,,, # eCard UMS notice validator aged-billings-payments [ecard-notice-validator] 2018-09-24T18:00:57Z Bill Erickson,,, # Aged money/billing schema and data +lost-paid-receipts [2.9-to-2.10-upgrade-reingest] 2017-07-03T20:10:59Z Bill Erickson,,, # Lost/Paid tracking and receipts +lost-paid-receipts-data [lost-paid-receipts] 2017-08-02T15:28:08Z Bill Erickson,,, # Lost/Paid permissions and receipts stock-browse-schema [ecard-notice-validator] 2018-08-31T15:22:58Z Bill Erickson,,, # Recover stock browse data tables, etc. stock-browse-headings-report [stock-browse-schema] 2018-10-04T15:56:18Z Bill Erickson,,, # New heading report updates for stock browse stock-browse-cleanup [stock-browse-schema] 2018-10-03T18:05:49Z Bill Erickson,,, # Delete old browse data diff --git a/KCLS/sql/schema/verify/lost-paid-receipts-data.sql b/KCLS/sql/schema/verify/lost-paid-receipts-data.sql new file mode 100644 index 0000000000..038a1b20ba --- /dev/null +++ b/KCLS/sql/schema/verify/lost-paid-receipts-data.sql @@ -0,0 +1,7 @@ +-- Verify kcls-evergreen:lost-paid-receipts-data on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/KCLS/sql/schema/verify/lost-paid-receipts.sql b/KCLS/sql/schema/verify/lost-paid-receipts.sql new file mode 100644 index 0000000000..091ff6d956 --- /dev/null +++ b/KCLS/sql/schema/verify/lost-paid-receipts.sql @@ -0,0 +1,7 @@ +-- Verify kcls-evergreen:lost-paid-receipts on pg + +BEGIN; + +-- XXX Add verifications here. + +ROLLBACK; diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 3551664795..769afb4d8f 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -7253,7 +7253,7 @@ SELECT usr, - + @@ -8097,7 +8097,7 @@ SELECT usr, - + @@ -8412,7 +8412,7 @@ SELECT usr, - + @@ -8559,7 +8559,7 @@ SELECT usr, - + @@ -12960,6 +12960,142 @@ SELECT usr, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + Mark As: + + + Pending + + + + Refunded + + + + Rejected + +
+
+ +
+
+

Transaction Summary

+
+
+ +
+
+
+
Transaction ID
+
{{mrxs.xact().id()}}
+
+
+
Amount Paid
+
{{mrxs.refundable_paid() | currency}}
+
+
+
B.O. Refund Amount
+
+ Rejected +
+
+ {{mrxs.refund_amount() | currency}} +
+
+ +
+
+
+
B.O. Action Date
+
{{mrxs.action_date() | date:'short'}}
+
+
+
Copy Barcode
+
{{mrxs.copy_barcode()}}
+
+
+
Title
+
{{mrxs.title()}}
+
+
+
Patron ID
+
{{mrxs.usr()}}
+
+
+
Patron Name
+
+ {{mrxs.usr_first_name()}} + {{mrxs.usr_middle_name()}} + {{mrxs.usr_family_name()}} +
+
+
+
Patron Address
+
+ {{mrxs.usr_street1()}} {{mrxs.usr_street2()}} + {{mrxs.usr_city()}}, {{mrxs.usr_state()}} {{mrxs.usr_post_code()}} +
+
+ +
+
+ +
+
+ +
+
+

Payments

+
+ + +
+
+
Receipt Code
+
{{mrps.receipt_code()}}
+
+
+
Amount
+
{{mrps.amount() | currency}}
+
+
+
Payment Date
+
{{mrps.payment_time() | date:'short'}}
+
+
+
Payment Type
+
+ + Cash + Check + Credit Card + {{mrps.payment_type()}} + +
+
+
+
Final Payment
+
+ Yes + No +
+
+
+
Refunded Via
+
+ + + Check + + + + Credit Card + +
+
+
+
Staff Name
+
{{mrps.staff_name()}}
+
+
+
Staff Email
+
{{mrps.staff_email()}}
+
+
+

+
+
+ +
+
+

All Transaction Billings and Payments

+
+ +
+
+

All Billings

+
+
Date
+
ID#
+
Amount
+
Type
+
Voided
+
Void Date
+
+
+
+
{{bill.billing_ts() | date:'shortDate'}}
+
{{bill.id()}}
+
{{bill.amount() | currency}}
+
{{bill.btype().name()}}
+
+
+ Yes +
+
+
+ {{bill.void_time() | date:'shortDate'}} +
+
+
+
+
+

All Payments

+
+
Date
+
ID#
+
Amount
+
Type
+
Voided
+
+
+
+
{{pay.payment_ts() | date:'shortDate'}}
+
{{pay.id()}}
+
{{pay.amount() | currency}}
+
+ + Cash + Check + Credit Card + {{pay.payment_type()}} + +
+
+
+ Yes +
+
+
+
+
+
+ + diff --git a/Open-ILS/src/templates/staff/circ/refunds/t_list.tt2 b/Open-ILS/src/templates/staff/circ/refunds/t_list.tt2 new file mode 100644 index 0000000000..00c77c9492 --- /dev/null +++ b/Open-ILS/src/templates/staff/circ/refunds/t_list.tt2 @@ -0,0 +1,85 @@ +
+ +
+ +
+
+ +
+ + + Limit to Refund Pending + + + + Limit to Last 12 Months + +
+
+
+ +
+
+ + + + + + + {{item._last_payment.receipt_code()}} ({{item.num_refundable_payments()}}) + + + {{item._last_payment.payment_time() | date:'short' }} + + + + + + + + + + + + + + + + + + +
+
+ diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2 index 4647ab3aa6..351292ee24 100644 --- a/Open-ILS/src/templates/staff/css/style.css.tt2 +++ b/Open-ILS/src/templates/staff/css/style.css.tt2 @@ -127,7 +127,11 @@ table.list tr.selected td { /* deprecated? */ .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; } diff --git a/Open-ILS/web/js/ui/default/staff/circ/refunds/app.js b/Open-ILS/web/js/ui/default/staff/circ/refunds/app.js new file mode 100644 index 0000000000..dec76162aa --- /dev/null +++ b/Open-ILS/web/js/ui/default/staff/circ/refunds/app.js @@ -0,0 +1,285 @@ +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(); +}]) + + diff --git a/Open-ILS/xul/staff_client/chrome/content/main/constants.js b/Open-ILS/xul/staff_client/chrome/content/main/constants.js index 9cb0936525..5d2ea5094a 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/constants.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/constants.js @@ -468,6 +468,7 @@ var urls = { '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', diff --git a/Open-ILS/xul/staff_client/server/patron/bill2.js b/Open-ILS/xul/staff_client/server/patron/bill2.js index ea7e128cd2..ab65647332 100644 --- a/Open-ILS/xul/staff_client/server/patron/bill2.js +++ b/Open-ILS/xul/staff_client/server/patron/bill2.js @@ -241,6 +241,128 @@ function event_listeners() { } } +/** + * 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() { @@ -920,6 +1042,7 @@ function verify_amount() { function apply_payment() { + try { var payment_blob = {}; JSAN.use('util.window'); @@ -982,6 +1105,10 @@ function apply_payment() { 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(); @@ -1105,6 +1232,11 @@ function pay(payment_blob) { 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; @@ -1129,6 +1261,29 @@ function pay(payment_blob) { } } +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) { diff --git a/Open-ILS/xul/staff_client/server/patron/bill_apply_payment_form.xul b/Open-ILS/xul/staff_client/server/patron/bill_apply_payment_form.xul new file mode 100644 index 0000000000..3636514514 --- /dev/null +++ b/Open-ILS/xul/staff_client/server/patron/bill_apply_payment_form.xul @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + +