@@ -, +, @@ package click on the "More" button on the right hand side of the button bar. You should see "Export patron's GDPR data" option. Selecting that will generate and download a JSON file containing the patron's data to your client machine. the "More" button in the patron page. --- installer/data/mysql/sysprefs.sql | 1 + .../prog/en/includes/members-toolbar.inc | 3 + .../prog/en/modules/admin/preferences/patrons.pref | 6 + koha-tmpl/intranet-tmpl/prog/js/members-menu.js | 8 + members/readingrec.pl | 252 ++++++++++++++++++++- 5 files changed, 269 insertions(+), 1 deletion(-) --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -17,6 +17,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('AllFinesNeedOverride','1','0','If on, staff will be asked to override every fine, even if it is below noissuescharge.','YesNo'), ('AllowAllMessageDeletion','0','','Allow any Library to delete any message','YesNo'), ('AllowFineOverride','0','0','If on, staff will be able to issue books to patrons with fines greater than noissuescharge.','YesNo'), +('AllowGDPRPatronExport','1','','If set all data for a patron can be exported from the staff interface.','YesNo'), ('AllowHoldDateInFuture','0','','If set a date field is displayed on the Hold screen of the Staff Interface, allowing the hold date to be set in the future.','YesNo'), ('AllowHoldItemTypeSelection','0','','If enabled, patrons and staff will be able to select the itemtype when placing a hold','YesNo'), ('AllowHoldPolicyOverride','0',NULL,'Allow staff to override hold policies when placing holds','YesNo'), --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -89,6 +89,9 @@
  • Export today's checked in barcodes
  • [% END %] [% END %] + [% IF Koha.Preference('AllowGDPRPatronExport') %] +
  • Export patron's GDPR data
  • + [% END %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -1,6 +1,12 @@ Patrons: General: - + - pref: AllowGDPRPatronExport + choices: + yes: "Allow export" + no: "Don't allow export" + - of patron data for GDPR compliance. + - - pref: AutoEmailOpacUser choices: yes: Send --- a/koha-tmpl/intranet-tmpl/prog/js/members-menu.js +++ a/koha-tmpl/intranet-tmpl/prog/js/members-menu.js @@ -55,6 +55,11 @@ $(document).ready(function(){ $(".btn-group").removeClass("open"); return false; }); + $("#exportgdprdata").click(function(){ + export_gdpr_data(); + $(".btn-group").removeClass("open"); + return false; + }); $("#printsummary").click(function(){ printx_window("page"); $(".btn-group").removeClass("open"); @@ -108,6 +113,9 @@ function confirm_reregistration() { function export_barcodes() { window.open('/cgi-bin/koha/members/readingrec.pl?borrowernumber=' + borrowernumber + '&op=export_barcodes'); } +function export_gdpr_data() { + window.open('/cgi-bin/koha/members/readingrec.pl?borrowernumber=' + borrowernumber + '&op=export_gdprdata'); +} var slip_re = /slip/; function printx_window(print_type) { var handler = print_type.match(slip_re) ? "printslip" : "summary-print"; --- a/members/readingrec.pl +++ a/members/readingrec.pl @@ -30,9 +30,12 @@ use C4::Members; use List::MoreUtils qw/any uniq/; use Koha::DateUtils; use C4::Members::Attributes qw(GetBorrowerAttributes); +use JSON; +use Data::Dumper; use Koha::Patrons; use Koha::Patron::Categories; +use Koha::Account::Lines; my $input = CGI->new; @@ -54,7 +57,6 @@ if ($input->param('borrowernumber')) { my $borrowernumber = $input->param('borrowernumber'); $patron = Koha::Patrons->find( $borrowernumber ); } - my $logged_in_user = Koha::Patrons->find( $loggedinuser ) or die "Not logged in"; output_and_exit_if_error( $input, $cookie, $template, { module => 'members', logged_in_user => $logged_in_user, current_patron => $patron } ); @@ -92,6 +94,254 @@ if ( $op eq 'export_barcodes' ) { } } +if($op eq 'export_gdprdata') { + my $exportedData = { + 'patron' => { + 'title' => $patron->title, + 'surname' => $patron->surname, + 'firstname' => $patron->firstname, + 'othernames' => $patron->othernames, + 'initials' => $patron->initials, + 'streetnumber' => $patron->streetnumber, + 'streettype' => $patron->streettype, + 'address' => $patron->address, + 'address2' => $patron->address2, + 'city' => $patron->city, + 'state' => $patron->state, + 'zipcode' => $patron->zipcode, + 'country' => $patron->country, + 'category_type' => $patron->category->category_type, + 'email' => $patron->email, + 'phone' => $patron->phone, + 'mobile' => $patron->mobile, + 'fax' => $patron->fax, + 'emailpro' => $patron->emailpro, + 'phonepro' => $patron->phonepro, + 'B_streetnumber' => $patron->B_streetnumber, + 'B_streettype' => $patron->B_streettype, + 'B_address' => $patron->B_address, + 'B_address2' => $patron->B_address2, + 'B_city' => $patron->B_city, + 'B_state' => $patron->B_state, + 'B_zipcode' => $patron->B_zipcode, + 'B_country' => $patron->B_country, + 'B_email' => $patron->B_email, + 'B_phone' => $patron->B_phone, + 'dateoffbirth' => $patron->dateofbirth, + 'categorycode' => $patron->categorycode, + 'dateenrolled' => $patron->dateenrolled, + 'dateexpiry' => $patron->dateexpiry, + 'date_renewed' => $patron->date_renewed, + 'gonenoaddress' => $patron->gonenoaddress, + 'lost' => $patron->lost, + 'debarred' => $patron->debarred, + 'debarredcomment' => $patron->debarredcomment, + 'contactname' => $patron->contactname, + 'contactfirstname' => $patron->contactfirstname, + 'contacttitle' => $patron->contacttitle, + 'borrowernotes' => $patron->borrowernotes, + 'sex' => $patron->sex, + 'userid' => $patron->userid, + 'opacnote' => $patron->opacnote, + 'contactnote' => $patron->contactnote, + 'altcontactfirstname' => $patron->altcontactfirstname, + 'altcontactsurname' => $patron->altcontactsurname, + 'altcontactaddress1' => $patron->altcontactaddress1, + 'altcontactaddress2' => $patron->altcontactaddress2, + 'altcontactaddress3' => $patron->altcontactaddress3, + 'altcontactstate' => $patron->altcontactstate, + 'altcontactzipcode' => $patron->altcontactzipcode, + 'altcontactcountry' => $patron->altcontactcountry, + 'altcontactphone' => $patron->altcontactphone, + 'smsalertnumber' => $patron->smsalertnumber, + 'updated_on' => $patron->updated_on, + 'lastseen' => $patron->lastseen, + 'lang' => $patron->lang, + 'login_attempts' => $patron->login_attempts, + 'account_locked' => $patron->account_locked, + 'age' => $patron->get_age, + 'has_overdues' => $patron->has_overdues, + 'borrowernumber' => $patron->borrowernumber, + 'cardnumber' => $patron->cardnumber, + 'branchcode' => $patron->branchcode, + }, + }; + + my $guarantor = $patron->guarantor(); + if($guarantor) { + $exportedData->{'patron'}->{'guarantor'} = + $guarantor->{'borrowernumber'}; + } + my @guarantees = $patron->guarantees(); + if(@guarantees) { + foreach my $bod (@guarantees) { + push @{$exportedData->{'patron'}->{'guarantees'}}, $bod->borrowernumber; + } + } + + my $image = $patron->image; + if($image) { + $exportedData->{'image'} = { + 'mimetype' => $image->mimetype, + 'imagefile' => $image->imagefile, + }; + } + + my $holds = $patron->holds; + if($holds) { + while(my $hold = $holds->next()) { + push @{$exportedData->{'holds'}}, { + 'reserve_id' => $hold->reserve_id, + 'reservedate' => $hold->reservedate, + 'biblionumber' => $hold->biblionumber, + 'branchcode' => $hold->branchcode, + 'nofificationdate' => $hold->notificationdate, + 'reminderdate' => $hold->reminderdate, + 'cancellationdate' => $hold->cancellationdate, + 'reservenotes' => $hold->reservenotes, + 'priority' => $hold->priority, + 'found' => $hold->found, + 'timestamp' => $hold->timestamp, + 'itemnumber' => $hold->itemnumber, + 'waitingdate' => $hold->waitingdate, + 'expirationdate' => $hold->expirationdate, + 'lowestPriority' => $hold->lowestPriority, + 'suspend' => $hold->suspend, + 'suspend_until' => $hold->suspend_until, + 'itemtype' => $hold->itemtype, + } + } + } + + my $oldHolds = $patron->old_holds; + if($oldHolds) { + while(my $hold = $oldHolds->next()) { + push @{$exportedData->{'holds'}}, { + 'reserve_id' => $hold->reserve_id, + 'reservedate' => $hold->reservedate, + 'biblionumber' => $hold->biblionumber, + 'branchcode' => $hold->branchcode, + 'nofificationdate' => $hold->notificationdate, + 'reminderdate' => $hold->reminderdate, + 'cancellationdate' => $hold->cancellationdate, + 'reservenotes' => $hold->reservenotes, + 'priority' => $hold->priority, + 'found' => $hold->found, + 'timestamp' => $hold->timestamp, + 'itemnumber' => $hold->itemnumber, + 'waitingdate' => $hold->waitingdate, + 'expirationdate' => $hold->expirationdate, + 'lowestPriority' => $hold->lowestPriority, + 'suspend' => $hold->suspend, + 'suspend_until' => $hold->suspend_until, + 'itemtype' => $hold->itemtype, + } + } + } + + my $checkouts = $patron->checkouts; + if($checkouts) { + while(my $checkout = $checkouts->next()) { + push @{$exportedData->{'checkouts'}}, { + 'issue_id' => $checkout->issue_id, + 'itemnumber' => $checkout->itemnumber, + 'date_due' => $checkout->date_due, + 'branchcode' => $checkout->branchcode, + 'returndate' => $checkout->returndate, + 'lastreneweddate' => $checkout->lastreneweddate, + 'renewals' =>$checkout->renewals, + 'auto_renew' => $checkout->auto_renew, + 'auto_renew_error' => $checkout->auto_renew_error, + 'timestamp' => $checkout->timestamp, + 'issuedate' => $checkout->issuedate, + 'onsite_checkout' => $checkout->onsite_checkout, + 'note' => $checkout->note, + 'notedate' => $checkout->notedate, + } + } + } + + my $oldCheckouts = $patron->old_checkouts; + if($oldCheckouts) { + while(my $checkout = $oldCheckouts->next()) { + push @{$exportedData->{'checkouts'}}, { + 'issue_id' => $checkout->issue_id, + 'itemnumber' => $checkout->itemnumber, + 'date_due' => $checkout->date_due, + 'branchcode' => $checkout->branchcode, + 'returndate' => $checkout->returndate, + 'lastreneweddate' => $checkout->lastreneweddate, + 'renewals' =>$checkout->renewals, + 'auto_renew' => $checkout->auto_renew, + 'auto_renew_error' => $checkout->auto_renew_error, + 'timestamp' => $checkout->timestamp, + 'issuedate' => $checkout->issuedate, + 'onsite_checkout' => $checkout->onsite_checkout, + 'note' => $checkout->note, + 'notedate' => $checkout->notedate, + } + } + } + + my $clubs = $patron->get_club_enrollments; + if($clubs) { + while(my $club = $clubs->next()) { + push @{$exportedData->{'clubs'}}, { + id => $club->id, + date_enrolled => $club->date_enrolled, + date_canceled => $club->date_canceled, + date_created => $club->date_created, + date_updated => $club->date_updated, + branchcode => $club->branchcode, + } + } + } + + my $account = $patron->account; + if($account) { + $exportedData->{'account'} = { + 'balance' => $account->balance, + 'non_issues_charges' => $account->non_issues_charges, + }; + my $acclines = Koha::Account::Lines->search( + { + borrowernumber => $patron->borrowernumber, + }, + { + order_by => 'accountno' + } + ); + while(my $thisLine = $acclines->next()) { + push @{$exportedData->{'account'}->{'accountLines'}}, { + 'accountlines_id' => $thisLine->accountlines_id, + 'payment_type' => $thisLine->payment_type, + 'amountoutstanding' => $thisLine->amountoutstanding, + 'accounttype' => $thisLine->accounttype, + 'timestamp' => $thisLine->timestamp, + 'issue_id' => $thisLine->issue_id, + 'description' => $thisLine->description, + 'lastincrement' => $thisLine->lastincrement, + 'date' => $thisLine->date, + 'amount' => $thisLine->amount, + 'manager_id' => $thisLine->manager_id, + 'note' => $thisLine->note, + } + } + } + + # Generate the JSON file and spit it out + my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }); + my $borrowercardnumber = $patron->cardnumber; + print $input->header( + -type => 'application/json', + -charset => 'utf-8', + -attachment => "$today-$borrowercardnumber-GDPR-Export.json" + ); + my $json = new JSON; + print $json->pretty->encode($exportedData); + exit; +} + if (! $limit){ $limit = 'full'; } --