Bugzilla – Attachment 47256 Details for
Bug 15654
Integrating a cash register system into Koha: An Example
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 15654: Integrate cash register system into Koha (An Example: case CPU)
Bug-15654-Integrate-cash-register-system-into-Koha.patch (text/plain), 82.56 KB, created by
Lari Taskula
on 2016-01-25 14:24:57 UTC
(
hide
)
Description:
Bug 15654: Integrate cash register system into Koha (An Example: case CPU)
Filename:
MIME Type:
Creator:
Lari Taskula
Created:
2016-01-25 14:24:57 UTC
Size:
82.56 KB
patch
obsolete
>From e8560852bee86575442c9fce9fdc5c6e66880a87 Mon Sep 17 00:00:00 2001 >From: Lari Taskula <larit@student.uef.fi> >Date: Fri, 16 Oct 2015 11:32:13 +0000 >Subject: [PATCH] Bug 15654: Integrate cash register system into Koha (An > Example: case CPU) > >Integrates Koha with cash register system. > >Adds "payments_transactions" and "payments_transactions_accountlines" >database tables to hold the cash register system payments. > >Adds REST API endpoints > - /api/v1/pos/cpu/100 GET information about a payment > - /api/v1/pos/cpu/100/report POST to tell Koha that payment is > completed / cancelled. > >Adds some configurations for the integration to koha-conf. > >Intranet template modifications for the integration. After payment is >sent to the cash register server, a loading screen is shown until the >payment is either completed or cancelled at the cash register. > >Adds system preferences to enable/disable the integration and to map >Koha fines to codes recognized by the cash register (as YAML-config). > >Ability to set SSL certificates for the connection between Koha server >and the cash register server. > >----------------------------------------------------------------------- > >Step by step how it works: >- Borrower wants to pay fines. >- Librarian confirms to pay the fines. >- Payment is created into payments_transactions. The selected > accountlines related to this payment are stored into > payments_transactions_accountlines. >- A new loading screen opens. An Ajax call is made to Koha server. >- Koha receives the call and starts to go through the selected payments. >- Koha "translates" Koha-fine-types (accounttypes) into codes recognized > by the cash register and forms a JSON object for the payment. >- JSON is sent as a long polling request to cash register server. >- Librarian completes payment at cash register. Koha receives a response. >- Ajax call gets an response and tells the librarian whether the payment > was completed/cancelled. >- REST API receives a message from the cash register server. The message > tells whether the payment was completed/cancelled. According to this > information, the payment at "payments_transactions" table gets an > updated status. >- Librarian is forwarded into Account tab of Borrower's Fines. If the > payment was paid, it should be now shown. > >----------------------------------------------------------------------- > >THIS PATCH ONLY SUPPORTS OUR LOCAL PROVIDER. It is provided as an example >in hope to start discussion whether a feature like this is wanted in the >Koha community. This patch cannot easily be tested with the actual cash >register server, because the server is not open source. However, if you >wish to proceed into actually testing the integration, leave a comment >in Bug 15654 and we can discuss it further. One possibility would be >to create a simple script that responds the way the integration expects. >--- > C4/Accounts.pm | 2 +- > C4/OPLIB/CPUIntegration.pm | 443 +++++++++++++++++++++ > Koha/PaymentsTransaction.pm | 271 +++++++++++++ > Koha/PaymentsTransactions.pm | 36 ++ > Koha/REST/V1/POSIntegration.pm | 57 +++ > Koha/Schema/Result/PaymentsTransaction.pm | 169 ++++++++ > .../Result/PaymentsTransactionsAccountline.pm | 112 ++++++ > api/v1/swagger.json | 129 ++++++ > etc/koha-conf.xml | 18 + > etc/koha-httpd.conf | 12 + > ...7-CPU_Integration-Add_table_for_transactions.pl | 51 +++ > installer/data/mysql/kohastructure.sql | 41 ++ > .../prog/en/modules/admin/preferences/tools.pref | 14 + > .../prog/en/modules/members/boraccount.tt | 49 ++- > .../prog/en/modules/members/paycollect.tt | 361 ++++++++++++++++- > members/boraccount.pl | 20 + > members/paycollect.pl | 44 ++ > t/db_dependent/CPUIntegration.t | 208 ++++++++++ > 18 files changed, 2032 insertions(+), 5 deletions(-) > create mode 100644 C4/OPLIB/CPUIntegration.pm > create mode 100644 Koha/PaymentsTransaction.pm > create mode 100644 Koha/PaymentsTransactions.pm > create mode 100644 Koha/REST/V1/POSIntegration.pm > create mode 100644 Koha/Schema/Result/PaymentsTransaction.pm > create mode 100644 Koha/Schema/Result/PaymentsTransactionsAccountline.pm > create mode 100644 installer/data/mysql/atomicupdate/KD#377-CPU_Integration-Add_table_for_transactions.pl > create mode 100644 t/db_dependent/CPUIntegration.t > >diff --git a/C4/Accounts.pm b/C4/Accounts.pm >index 4f5f8d4..7079815 100644 >--- a/C4/Accounts.pm >+++ b/C4/Accounts.pm >@@ -601,7 +601,7 @@ sub recordpayment_selectaccts { > my $dbh = C4::Context->dbh; > my $newamtos = 0; > my $accdata = q{}; >- my $branch = C4::Context->userenv->{branch}; >+ my $branch = C4::Context->userenv->{branch} if C4::Context->userenv; > my $amountleft = $amount; > my $manager_id = 0; > $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; >diff --git a/C4/OPLIB/CPUIntegration.pm b/C4/OPLIB/CPUIntegration.pm >new file mode 100644 >index 0000000..6373c02 >--- /dev/null >+++ b/C4/OPLIB/CPUIntegration.pm >@@ -0,0 +1,443 @@ >+package C4::OPLIB::CPUIntegration; >+ >+use Modern::Perl; >+ >+use C4::Accounts; >+use C4::Branch; >+use C4::Context; >+use C4::Log; >+ >+use Data::Dumper qw(Dumper); >+use Digest::SHA qw(sha256_hex); >+use Encode; >+use Net::SSL; >+use YAML::XS; >+ >+use Koha::Borrower; >+use Koha::Borrowers; >+use Koha::PaymentsTransaction; >+use Koha::PaymentsTransactions; >+ >+use Koha::Exception::NoSystemPreference; >+ >+use bignum; >+ >+=head1 FUNCTIONS >+ >+=head2 InitializePayment >+ >+ &InitializePayment($args); >+ >+Initializes the accountlines that will be sent to CPU. >+ >+Returns the payment HASH. >+ >+=cut >+ >+sub InitializePayment { >+ my ($args) = shift; >+ >+ my $dbh = C4::Context->dbh; >+ my $borrowernumber = $args->{borrowernumber}; >+ my @selected = @{ $args->{selected} }; >+ >+ # Hash containing CPU format of payment >+ my $payment; >+ $payment->{Office} = $args->{office}; >+ $payment->{Products} = []; >+ >+ @selected = sort { $a <=> $b } @selected if @selected > 1; >+ >+ my $total_price = 0; >+ my $money_left = _convert_to_cents($args->{total_paid}); >+ >+ my $use_selected = (@selected > 0) ? "AND accountlines_id IN (?".+(",?") x (@selected-1).")" : ""; >+ my $sql = "SELECT * FROM accountlines WHERE borrowernumber=? AND (amountoutstanding<>0) ".$use_selected." ORDER BY date"; >+ my $sth = $dbh->prepare($sql); >+ >+ $sth->execute($borrowernumber, @selected); >+ >+ # Create a new transaction >+ my $transaction = Koha::PaymentsTransaction->new()->set({ >+ borrowernumber => $borrowernumber, >+ status => "unsent", >+ description => $args->{payment_note} || '', >+ })->store(); >+ >+ while ( (my $accdata = $sth->fetchrow_hashref) and $money_left > 0) { >+ my $product; >+ >+ $product->{Code} = $accdata->{'accounttype'}; >+ $product->{Amount} = 1; >+ $product->{Description} = $accdata->{'description'}; >+ >+ if ( _convert_to_cents($accdata->{'amountoutstanding'}) >= $money_left ) { >+ $product->{Price} = $money_left; >+ $money_left = 0; >+ } else { >+ $product->{Price} = _convert_to_cents($accdata->{'amountoutstanding'}); >+ $money_left -= _convert_to_cents($accdata->{'amountoutstanding'}); >+ } >+ push $payment->{Products}, $product; >+ $total_price += $product->{Price}; >+ >+ $transaction->AddRelatedAccountline($accdata->{'accountlines_id'}, $product->{Price}); >+ } >+ >+ $transaction->set({ price_in_cents => $total_price })->store(); >+ >+ my $borrower = Koha::Borrowers->cast($transaction->borrowernumber); >+ >+ my $description = $borrower->surname . ", " . $borrower->firstname . " (".$borrower->cardnumber.")"; >+ >+ $payment->{ApiVersion} = "2.0"; >+ $payment->{Source} = C4::Context->config('pos')->{'CPU'}->{'source'}; >+ $payment->{Id} = $transaction->transaction_id; >+ $payment->{Mode} = C4::Context->config('pos')->{'CPU'}->{'mode'}; >+ $payment->{Description} = $description; >+ $payment->{Products} = AccountTypesToItemNumbers($transaction->GetProducts(), C4::Branch::mybranch()); >+ >+ my $notificationAddress = C4::Context->config('pos')->{'CPU'}->{'notificationAddress'}; >+ my $transactionNumber = $transaction->transaction_id; >+ $notificationAddress =~ s/{invoicenumber}/$transactionNumber/g; >+ >+ $payment->{NotificationAddress} = $notificationAddress; # url for report >+ >+ $payment = _validate_cpu_hash($payment); # Remove semicolons >+ $payment->{Hash} = CalculatePaymentHash($payment); >+ >+ $payment = _validate_cpu_hash($payment); # Convert strings to int >+ $payment->{"send_payment"} = "POST"; >+ >+ return $payment; >+} >+ >+=head2 SendPayment >+ >+ SendPayment($payment); >+ >+Sends a payment to CPU. $payment is a HASH that needs to be in the CPU format with >+SHA-256 hash calculated correctly. >+ >+Returns JSON-encoded response from CPU. See the CPU document for response protocol. >+ >+=cut >+ >+sub SendPayment { >+ my $content = shift; >+ my $response; >+ >+ $response = eval { >+ my $payment = $content; >+ >+ delete $payment->{send_payment} if $payment->{send_payment}; >+ >+ # Convert strings to integer for JSON >+ $payment = _validate_cpu_hash($payment); >+ >+ # Construct JSON object >+ $content = JSON->new->utf8->canonical(1)->encode($payment); >+ >+ my $transaction = Koha::PaymentsTransactions->find($payment->{Id}); >+ >+ if (C4::Context->config('pos')->{'CPU'}->{'ssl_cert'}) { >+ # Define SSL certificate >+ $ENV{HTTPS_CERT_FILE} = C4::Context->config('pos')->{'CPU'}->{'ssl_cert'}; >+ $ENV{HTTPS_KEY_FILE} = C4::Context->config('pos')->{'CPU'}->{'ssl_key'}; >+ $ENV{HTTPS_CA_FILE} = C4::Context->config('pos')->{'CPU'}->{'ssl_ca_file'}; >+ } >+ >+ my $ua = LWP::UserAgent->new; >+ >+ if (C4::Context->config('pos')->{'CPU'}->{'ssl_cert'}) { >+ $ua->ssl_opts({ >+ SSL_use_cert => 1, >+ }); >+ } >+ >+ $ua->timeout(500); >+ >+ my $req = HTTP::Request->new(POST => C4::Context->config('pos')->{'CPU'}->{'url'}); >+ $req->header('content-type' => 'application/json'); >+ $req->content($content); >+ >+ $transaction->set({ status => "pending" })->store(); >+ >+ my $request = $ua->request($req); >+ >+ # There is an issue where the call above fails for unknown reasons, but REST API got >+ # confirmation of successful payment. We need to be able to recognize payments >+ # that have been completed during $ua->request($req) by REST API and not set them to >+ # "cancelled" status even if $ua->request($req) returns some HTTP error code. >+ # At this point, payment should still be "pending". Refresh payment status. >+ >+ $transaction = Koha::PaymentsTransactions->find($payment->{Id}); >+ my $payment_already_paid = 1 if $transaction->status eq "paid"; # Already paid via REST API! >+ return JSON->new->utf8->canonical(1)->encode({ Status => '1' }) if $payment_already_paid; >+ >+ if ($request->{_rc} != 200) { >+ # Did not get HTTP 200, some error happened! >+ $transaction->set({ status => "cancelled", description => $request->{_content} })->store(); >+ return JSON->new->utf8->canonical(1)->encode({ error => $request->{_content}, Status => '89' }); >+ } >+ >+ my $response = JSON->new->utf8->canonical(1)->decode($request->{_content}); >+ >+ # Calculate response checksum and return error if they do not match >+ my $hash = CalculateResponseHash($response); >+ >+ if ($hash ne $response->{Hash}) { >+ $transaction->set({ status => "cancelled", description => "Invalid hash" })->store(); >+ return JSON->new->utf8->canonical(1)->encode({ error => "Invalid hash", Status => $response->{Status} }); >+ } >+ >+ return JSON->new->utf8->canonical(1)->encode($response); >+ }; >+ >+ if ($@) { >+ my $transaction = Koha::PaymentsTransactions->find($content->{Id}); >+ my $payment_already_paid = 1 if $transaction->status eq "paid"; # Already paid via REST API! >+ return JSON->new->utf8->canonical(1)->encode({ Status => '1' }) if $payment_already_paid; >+ $transaction->set({ status => "cancelled", description => $@ })->store(); >+ return JSON->new->utf8->canonical(1)->encode({ error => "Error: " . $@, Status => '88' }); >+ } >+ >+ return $response; >+} >+ >+=head2 HandleResponseStatus >+ >+ HandleResponseStatus($code, $transaction) >+ >+Sets the correct transaction status according to the status code in CPU response. >+ >+Returns a Koha::PaymentsTransaction object >+ >+=cut >+sub HandleResponseStatus { >+ my ($code, $transaction) = @_; >+ >+ my $status = getResponseString($code); >+ >+ $transaction->set($status)->store(); # set the status >+ >+ return $transaction; >+} >+ >+=head2 GetResponseString >+ >+ GetResponseString($statuscode) >+ >+ Converts CPU Status code into string recognized by payments_transactions.status >+ e.g. paid, cancelled, pending >+ >+Returns status as string >+ >+=cut >+sub GetResponseString { >+ my ($code) = @_; >+ >+ my $status; >+ $status->{status} = "cancelled"; # default status >+ >+ if ($code == 0) { >+ # Payment was cancelled >+ } >+ elsif ($code == 1) { >+ # Payment was successful >+ $status->{status} = "paid"; >+ } >+ elsif ($code == 2) { >+ # Payment is pending >+ $status->{status} = "pending"; >+ } >+ elsif ($code == 97) { >+ # Id was duplicate (duplicate transaction id - different hash) >+ $status->{description} = "ERROR 97: Duplicate id"; >+ } >+ elsif ($code == 98) { >+ # System error >+ $status->{description} = "ERROR 98: System error"; >+ } >+ elsif ($code == 99) { >+ # Invalid invoice >+ $status->{description} = "ERROR 99: Invalid invoice"; >+ } >+ else { >+ $status->{description} = "Unknown status"; >+ } >+ >+ return $status; >+} >+ >+=head2 hasBranchEnabledIntegration >+ >+ hasBranchEnabledIntegration($branch); >+ >+ Checks if the $branch has enabled POS integration. Integration is enabled >+ when the systempreference "cpuitemnumber" YAML config has mapping of >+ Koha-itemtypes to CPU-itemnumbers for $branch. >+ >+Returns 1 if yes, otherwise 0. >+ >+=cut >+sub hasBranchEnabledIntegration { >+ my ($branch) = @_; >+ >+ # Load YAML conf from syspref cpuitemnumbers >+ my $pref = C4::Context->preference("cpuitemnumbers"); >+ return 0 unless $pref; >+ my $config = YAML::XS::Load( >+ Encode::encode( >+ 'UTF-8', >+ $pref, >+ Encode::FB_CROAK >+ )); >+ >+ return 0 unless $config->{$branch}; >+ return 1; >+} >+ >+=head2 AccountTypesToItemNumbers >+ >+ AccountTypesToItemNumbers($products, $branch); >+ >+Maps Koha-itemtypes (accountlines.accounttype) to CPU itemnumbers. >+ >+This is defined in system preference "cpuitemnumbers". >+ >+Products is an array of Product (HASH) that are in the format of CPU-document. >+ >+Returns an ARRAY of products (HASH). >+ >+=cut >+sub AccountTypesToItemNumbers { >+ my ($products, $branch) = @_; >+ >+ # Load YAML conf from syspref cpuitemnumbers >+ my $pref = C4::Context->preference("cpuitemnumbers"); >+ Koha::Exception::NoSystemPreference->throw( error => "YAML configuration in system preference 'cpuitemnumbers' is not defined! Cannot assign item numbers for accounttypes." ) unless $pref; >+ my $config = YAML::XS::Load( >+ Encode::encode( >+ 'UTF-8', >+ $pref, >+ Encode::FB_CROAK >+ )); >+ >+ Koha::Exception::NoSystemPreference->throw( error => "No item number configuration for branch '".$branch."'. Configure system preference 'cpuitemnumbers'") unless $config->{$branch}; >+ >+ my $modified_products; >+ >+ for my $product (@$products){ >+ my $mapped_product = $product; >+ >+ # If accounttype is mapped to an item number >+ if ($config->{$branch}->{$product->{Code}}) { >+ $mapped_product->{Code} = $config->{$branch}->{$product->{Code}} >+ } else { >+ # Else, try to use accounttype "Default" >+ Koha::Exception::NoSystemPreference->throw( error => "Could not assign item number to accounttype '".$product->{Code}."'. Configure system preference 'cpuitemnumbers' with parameters 'Default'.") unless $config->{$branch}->{'Default'}; >+ >+ $mapped_product->{Code} = $config->{$branch}->{'Default'}; >+ } >+ >+ push @$modified_products, $mapped_product; >+ } >+ >+ return $modified_products; >+} >+ >+ >+=head2 CalculatePaymentHash >+ >+ CalculatePaymentHash($response); >+ >+Calculates SHA-256 hash from our payment hash. Returns the SHA-256 string. >+ >+=cut >+ >+sub CalculatePaymentHash { >+ my $invoice = shift; >+ my $data; >+ >+ foreach my $param (sort keys $invoice){ >+ next if $param eq "Hash"; >+ my $value = $invoice->{$param}; >+ >+ if (ref($invoice->{$param}) eq 'ARRAY') { >+ my $product_hash = $value; >+ $value = ""; >+ foreach my $product (values $product_hash){ >+ foreach my $product_data (sort keys $product){ >+ $value .= $product->{$product_data} . "&"; >+ } >+ } >+ $value =~ s/&$//g >+ } >+ >+ $data .= $value . "&"; >+ } >+ >+ $data .= C4::Context->config('pos')->{'CPU'}->{'secretKey'}; >+ $data = Encode::encode_utf8($data); >+ return Digest::SHA::sha256_hex($data); >+} >+ >+=head2 CalculateResponseHash >+ >+ CalculateResponseHash($response); >+ >+Calculates SHA-256 hash from CPU's response. Returns the SHA-256 string. >+ >+=cut >+ >+sub CalculateResponseHash { >+ my $resp = shift; >+ my $data = ""; >+ >+ $data .= $resp->{Source} if defined $resp->{Source}; >+ $data .= "&" . $resp->{Id} if defined $resp->{Id}; >+ $data .= "&" . $resp->{Status} if defined $resp->{Status}; >+ $data .= "&" . $resp->{Reference} if defined $resp->{Reference}; >+ $data .= "&" . C4::Context->config('pos')->{'CPU'}->{'secretKey'}; >+ >+ $data =~ s/^&//g; >+ >+ $data = Digest::SHA::sha256_hex($data); >+ return $data; >+} >+ >+sub _validate_cpu_hash { >+ my $invoice = shift; >+ >+ # CPU does not like a semicolon. Go through the fields and make sure >+ # none of the fields contain ';' character (from CPU documentation) >+ foreach my $field (keys $invoice){ >+ $invoice->{$field} =~ s/;/\x{037E}/g; # Replace semicolon with a Greek question mark (;) >+ } >+ >+ $invoice->{Mode} = int($invoice->{Mode}); >+ foreach my $product (@{ $invoice->{Products} }){ >+ foreach my $product_field (keys $product){ >+ $product->{$product_field} =~ s/;/\x{037E}/g; # Replace semicolon with a Greek question mark (;) >+ } >+ $product->{Amount} = int($product->{Amount}) if $product->{Amount}; >+ $product->{Price} = int($product->{Price}) if $product->{Price}; >+ } >+ >+ return $invoice; >+} >+ >+sub _convert_to_cents { >+ my ($price) = @_; >+ >+ return int($price*100); # transform into cents >+} >+ >+sub _convert_to_euros { >+ my ($price) = @_; >+ >+ return $price/100; >+} >+ >+1; >diff --git a/Koha/PaymentsTransaction.pm b/Koha/PaymentsTransaction.pm >new file mode 100644 >index 0000000..5178556 >--- /dev/null >+++ b/Koha/PaymentsTransaction.pm >@@ -0,0 +1,271 @@ >+package Koha::PaymentsTransaction; >+ >+# Copyright Open Source Freedom Fighters >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or (at your option) any later >+# version. >+# >+# Koha 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. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+use Data::Dumper; >+ >+use C4::Accounts; >+use C4::Context; >+use C4::Log; >+use C4::Stats; >+ >+use Koha::Database; >+ >+use bignum; >+ >+use base qw(Koha::Object); >+ >+sub type { >+ return 'PaymentsTransaction'; >+} >+ >+sub AddRelatedAccountline { >+ my ($self, $accountlines_id, $paid_price) = @_; >+ >+ return 0 unless defined $accountlines_id and defined $paid_price; >+ >+ my $dbh = C4::Context->dbh; >+ my $sql = "INSERT INTO payments_transactions_accountlines (transaction_id, accountlines_id, paid_price_cents) VALUES (?, ?, ?)"; >+ >+ my $sth = $dbh->prepare($sql); >+ $sth->execute($self->transaction_id, $accountlines_id, $paid_price); >+ >+ return $dbh->last_insert_id(undef,undef,'payments_transactions_accountlines',undef); >+} >+ >+sub GetRelatedAccountlines { >+ my ($self) = @_; >+ >+ my $dbh = C4::Context->dbh; >+ my $sql = "SELECT accountlines.accountlines_id, accountlines.amountoutstanding, accountlines.accountno, payments_transactions_accountlines.paid_price_cents, payments_transactions_accountlines.transaction_id, accountlines.description, accountlines.itemnumber FROM accountlines INNER JOIN payments_transactions_accountlines >+ON payments_transactions_accountlines.accountlines_id = accountlines.accountlines_id AND payments_transactions_accountlines.transaction_id=?"; >+ my $sth = $dbh->prepare($sql); >+ $sth->execute($self->transaction_id); >+ >+ my $hash_ref = $sth->fetchall_arrayref({}); >+ $sth->finish; >+ return $hash_ref; >+} >+ >+sub GetProducts { >+ my ($self) = @_; >+ >+ my $dbh = C4::Context->dbh; >+ my $sql = "SELECT accountlines.accounttype, payments_transactions_accountlines.paid_price_cents, accountlines.description FROM accountlines INNER JOIN payments_transactions_accountlines >+ON payments_transactions_accountlines.accountlines_id = accountlines.accountlines_id AND payments_transactions_accountlines.transaction_id=?"; >+ my $sth = $dbh->prepare($sql); >+ $sth->execute($self->transaction_id); >+ >+ my @products; >+ >+ while (my $accountline = $sth->fetchrow_hashref) { >+ my $product; >+ $product->{Code} = $accountline->{'accounttype'}; >+ $product->{Price} = $accountline->{'paid_price_cents'}; >+ $product->{Description} = $accountline->{'description'}; >+ push @products, $product; >+ } >+ >+ return \@products; >+} >+ >+=head2 CompletePayment >+ >+ &CompletePayment($transaction_number); >+ >+Completes the payment in Koha from the given transaction number. >+ >+This subroutine will be called after payment is completed, >+(after payment report is received to REST API) >+ >+=cut >+ >+sub CompletePayment { >+ my ($self, $status) = @_; >+ my $dbh = C4::Context->dbh; >+ my $manager_id = 0; >+ $manager_id = C4::Context->userenv->{'number'} if C4::Context->userenv; >+ my $branch = C4::Context->userenv->{'branch'} if C4::Context->userenv; >+ my $description = ""; >+ my $itemnumber; >+ my $old_status; >+ my $new_status; >+ >+ my $transaction = $self; >+ return if not $transaction; >+ >+ # It's important that we don't process this subroutine twice at the same time! >+ $transaction = Koha::PaymentsTransactions->find($transaction->transaction_id); >+ >+ $old_status = $transaction->status; >+ $new_status = $status->{status}; >+ >+ if ($old_status eq $new_status){ >+ # Trying to complete with same status, makes no sense >+ return; >+ } >+ >+ if ($old_status ne "processing"){ >+ $transaction->set({ status => "processing" })->store(); >+ } else { >+ # Another process is already processing the payment >+ return; >+ } >+ >+ # Defined accountlines_id means that the payment is already completed in Koha. >+ # We don't want to make duplicate payments. So make sure it is not defined! >+ #return if defined $transaction->accountlines_id; >+ # Reverse the payment if old status is different than new status (and either paid or cancelled) >+ if (defined $transaction->accountlines_id && (($old_status eq "paid" and $new_status eq "cancelled") or ($old_status eq "cancelled" and $new_status eq "paid"))){ >+ C4::Accounts::ReversePayment($transaction->accountlines_id); >+ $transaction->set($status)->store(); >+ return; >+ } >+ >+ # Payment was cancelled >+ if ($new_status eq "cancelled") { >+ $transaction->set({ status => "cancelled" })->store(); >+ &logaction( >+ "PAYMENTS", >+ "PAY", >+ $transaction->transaction_id, >+ $transaction->status >+ ); >+ return; >+ } >+ >+ # If transaction is found, pay the accountlines associated with the transaction. >+ my $accountlines = $transaction->GetRelatedAccountlines(); >+ >+ # Define a variable for leftovers. This should not be needed, but it's a fail-safe. >+ my $leftovers = 0; >+ >+ my $sth = $dbh->prepare('UPDATE accountlines SET amountoutstanding= ? ' . >+ 'WHERE accountlines_id=?'); >+ >+ my @ids; >+ foreach my $acct (@$accountlines){ >+ if (_convert_to_cents($acct->{amountoutstanding}) == 0) { >+ $leftovers += _convert_to_euros($acct->{paid_price_cents}); >+ next; >+ } >+ >+ my $paidamount = _convert_to_euros($acct->{paid_price_cents}); >+ my $newamount = 0; >+ >+ $itemnumber = $acct->{itemnumber} if @$accountlines == 1; >+ >+ if ($acct->{amountoutstanding} >= $paidamount) { >+ $newamount = $acct->{amountoutstanding}-$paidamount; >+ } >+ else { >+ $leftovers += $paidamount-$acct->{amountoutstanding}; >+ } >+ >+ $sth->execute( $newamount, $acct->{accountlines_id} ); >+ >+ $description .= ((length($description) > 0) ? "\n" : "") . $acct->{description}; >+ >+ if ( C4::Context->preference("FinesLog") ) { >+ C4::Log::logaction("FINES", 'MODIFY', $transaction->borrowernumber, Dumper({ >+ action => 'fee_payment', >+ borrowernumber => $transaction->borrowernumber, >+ old_amountoutstanding => $acct->{'amountoutstanding'}, >+ new_amountoutstanding => $newamount, >+ amount_paid => $paidamount, >+ accountlines_id => $acct->{'accountlines_id'}, >+ accountno => $acct->{'accountno'}, >+ manager_id => $manager_id, >+ })); >+ push( @ids, $acct->{'accountlines_id'} ); >+ } >+ } >+ >+ if ($leftovers > 0) { >+ C4::Accounts::recordpayment_selectaccts($transaction->borrowernumber, $leftovers, [], "Leftovers from transaction ".$transaction->transaction_id); >+ $transaction->set({ status => $new_status })->store(); >+ } >+ >+ if ($transaction->price_in_cents-_convert_to_cents($leftovers) > 0) { >+ my $nextacctno = C4::Accounts::getnextacctno($transaction->borrowernumber); >+ # create new line >+ my $sql = 'INSERT INTO accountlines ' . >+ '(borrowernumber, accountno,date,amount,description,accounttype,amountoutstanding,itemnumber,manager_id,note) ' . >+ q|VALUES (?,?,now(),?,?,'Pay',?,?,?,?)|; >+ $dbh->do($sql,{},$transaction->borrowernumber, $nextacctno , (-1)*_convert_to_euros($transaction->price_in_cents-_convert_to_cents($leftovers)), $description, 0, $itemnumber, $manager_id, $transaction->description); >+ >+ $transaction->set({ status => $new_status, accountlines_id => $dbh->last_insert_id( undef, undef, 'accountlines', undef ) })->store(); >+ >+ C4::Stats::UpdateStats($branch, 'payment', _convert_to_euros($transaction->price_in_cents), '', '', '', $transaction->borrowernumber, $nextacctno); >+ >+ if ( C4::Context->preference("FinesLog") ) { >+ C4::Log::logaction("FINES", 'CREATE',$transaction->borrowernumber,Dumper({ >+ action => 'create_payment', >+ borrowernumber => $transaction->borrowernumber, >+ accountno => $nextacctno, >+ amount => 0 - _convert_to_euros($transaction->price_in_cents), >+ amountoutstanding => 0 - $leftovers, >+ accounttype => 'Pay', >+ accountlines_paid => \@ids, >+ manager_id => $manager_id, >+ })); >+ } >+ &logaction( >+ "PAYMENTS", >+ "PAY", >+ $transaction->transaction_id, >+ $transaction->status >+ ); >+ } >+} >+ >+=head2 RevertPayment >+ >+ &RevertPayment(); >+ >+Reverts the already completed payment. >+ >+=cut >+ >+sub RevertPayment { >+ my ($self) = @_; >+ my $dbh = C4::Context->dbh; >+ >+ my $transaction = $self; >+ >+ return if not $transaction; >+ >+ return if not defined $transaction->accountlines_id; >+ >+ C4::Accounts::ReversePayment($transaction->accountlines_id); >+} >+ >+ >+sub _convert_to_cents { >+ my ($price) = @_; >+ >+ return int($price*100); # transform into cents >+} >+ >+sub _convert_to_euros { >+ my ($price) = @_; >+ >+ return $price/100; >+} >+1; >\ No newline at end of file >diff --git a/Koha/PaymentsTransactions.pm b/Koha/PaymentsTransactions.pm >new file mode 100644 >index 0000000..5cac117 >--- /dev/null >+++ b/Koha/PaymentsTransactions.pm >@@ -0,0 +1,36 @@ >+package Koha::PaymentsTransactions; >+ >+# Copyright Open Source Freedom Fighters >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or (at your option) any later >+# version. >+# >+# Koha 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. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+use Koha::Database; >+use base qw(Koha::Objects); >+ >+sub type { >+ return 'PaymentsTransaction'; >+} >+ >+sub object_class { >+ return 'Koha::PaymentsTransaction'; >+} >+ >+sub _get_castable_unique_columns { >+ return ['transaction_id']; >+} >+ >+1; >\ No newline at end of file >diff --git a/Koha/REST/V1/POSIntegration.pm b/Koha/REST/V1/POSIntegration.pm >new file mode 100644 >index 0000000..611d33b >--- /dev/null >+++ b/Koha/REST/V1/POSIntegration.pm >@@ -0,0 +1,57 @@ >+package Koha::REST::V1::POSIntegration; >+ >+use Modern::Perl; >+use Mojo::Base 'Mojolicious::Controller'; >+ >+use C4::Log; >+use C4::OPLIB::CPUIntegration; >+ >+use Koha::PaymentsTransaction; >+use Koha::PaymentsTransactions; >+ >+sub get_transaction { >+ my ($c, $args, $cb) = @_; >+ >+ return $c->$cb({ error => "Missing transaction number"}, 400) if not $args->{'invoicenumber'}; >+ >+ # Find transaction >+ my $transaction = Koha::PaymentsTransactions->find($args->{invoicenumber}); >+ >+ return $c->$cb({ error => "Transaction not found"}, 404) if not $transaction; >+ >+ return $c->$cb({ >+ transaction_id => $transaction->transaction_id, >+ borrowernumber => $transaction->borrowernumber, >+ status => $transaction->status, >+ timestamp => $transaction->timestamp, >+ description => $transaction->description || "", >+ price_in_cents => int($transaction->price_in_cents), >+ }, 200); >+} >+ >+ >+=head2 CPU_report($c, $args, $cb) >+ >+Receives the success report from CPU. >+ >+=cut >+sub cpu_report { >+ my ($c, $args, $cb) = @_; >+ >+ my $invoicenumber = $args->{'invoicenumber'}; >+ $args = $args->{body}; >+ >+ # Check that the request is valid >+ return $c->$cb({ error => "Invalid Hash" }, 400) if C4::OPLIB::CPUIntegration::CalculateResponseHash($args) ne $args->{Hash}; >+ >+ # Find the transaction >+ my $transaction = Koha::PaymentsTransactions->find($invoicenumber); >+ return $c->$cb({ error => "Transaction not found"}, 404) if not $transaction; >+ >+ my $report_status = C4::OPLIB::CPUIntegration::GetResponseString($args->{Status}); >+ $transaction->CompletePayment($report_status); >+ >+ return $c->$cb("", 200); >+} >+ >+1; >diff --git a/Koha/Schema/Result/PaymentsTransaction.pm b/Koha/Schema/Result/PaymentsTransaction.pm >new file mode 100644 >index 0000000..6b8301c >--- /dev/null >+++ b/Koha/Schema/Result/PaymentsTransaction.pm >@@ -0,0 +1,169 @@ >+use utf8; >+package Koha::Schema::Result::PaymentsTransaction; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::PaymentsTransaction >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<payments_transactions> >+ >+=cut >+ >+__PACKAGE__->table("payments_transactions"); >+ >+=head1 ACCESSORS >+ >+=head2 transaction_id >+ >+ data_type: 'integer' >+ is_auto_increment: 1 >+ is_nullable: 0 >+ >+=head2 borrowernumber >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 accountlines_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 1 >+ >+=head2 status >+ >+ data_type: 'enum' >+ default_value: 'pending' >+ extra: {list => ["paid","pending","cancelled","unsent","processing"]} >+ is_nullable: 1 >+ >+=head2 timestamp >+ >+ data_type: 'timestamp' >+ datetime_undef_if_invalid: 1 >+ default_value: current_timestamp >+ is_nullable: 0 >+ >+=head2 description >+ >+ data_type: 'text' >+ is_nullable: 0 >+ >+=head2 price_in_cents >+ >+ data_type: 'integer' >+ is_nullable: 0 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "transaction_id", >+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, >+ "borrowernumber", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "accountlines_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, >+ "status", >+ { >+ data_type => "enum", >+ default_value => "pending", >+ extra => { >+ list => ["paid", "pending", "cancelled", "unsent", "processing"], >+ }, >+ is_nullable => 1, >+ }, >+ "timestamp", >+ { >+ data_type => "timestamp", >+ datetime_undef_if_invalid => 1, >+ default_value => \"current_timestamp", >+ is_nullable => 0, >+ }, >+ "description", >+ { data_type => "text", is_nullable => 0 }, >+ "price_in_cents", >+ { data_type => "integer", is_nullable => 0 }, >+); >+ >+=head1 PRIMARY KEY >+ >+=over 4 >+ >+=item * L</transaction_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("transaction_id"); >+ >+=head1 RELATIONS >+ >+=head2 accountline >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Accountline> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "accountline", >+ "Koha::Schema::Result::Accountline", >+ { accountlines_id => "accountlines_id" }, >+ { >+ is_deferrable => 1, >+ join_type => "LEFT", >+ on_delete => "CASCADE", >+ on_update => "RESTRICT", >+ }, >+); >+ >+=head2 borrowernumber >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Borrower> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "borrowernumber", >+ "Koha::Schema::Result::Borrower", >+ { borrowernumber => "borrowernumber" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+=head2 payments_transactions_accountlines >+ >+Type: has_many >+ >+Related object: L<Koha::Schema::Result::PaymentsTransactionsAccountline> >+ >+=cut >+ >+__PACKAGE__->has_many( >+ "payments_transactions_accountlines", >+ "Koha::Schema::Result::PaymentsTransactionsAccountline", >+ { "foreign.transaction_id" => "self.transaction_id" }, >+ { cascade_copy => 0, cascade_delete => 0 }, >+); >+ >+ >+# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-12-10 17:49:20 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:TtY1X7ynTADbZtcFxzeKeg >+ >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/Koha/Schema/Result/PaymentsTransactionsAccountline.pm b/Koha/Schema/Result/PaymentsTransactionsAccountline.pm >new file mode 100644 >index 0000000..832857f >--- /dev/null >+++ b/Koha/Schema/Result/PaymentsTransactionsAccountline.pm >@@ -0,0 +1,112 @@ >+use utf8; >+package Koha::Schema::Result::PaymentsTransactionsAccountline; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::PaymentsTransactionsAccountline >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<payments_transactions_accountlines> >+ >+=cut >+ >+__PACKAGE__->table("payments_transactions_accountlines"); >+ >+=head1 ACCESSORS >+ >+=head2 transactions_accountlines_id >+ >+ data_type: 'integer' >+ is_auto_increment: 1 >+ is_nullable: 0 >+ >+=head2 transaction_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 accountlines_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 paid_price_cents >+ >+ data_type: 'integer' >+ is_nullable: 0 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "transactions_accountlines_id", >+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, >+ "transaction_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "accountlines_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "paid_price_cents", >+ { data_type => "integer", is_nullable => 0 }, >+); >+ >+=head1 PRIMARY KEY >+ >+=over 4 >+ >+=item * L</transactions_accountlines_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("transactions_accountlines_id"); >+ >+=head1 RELATIONS >+ >+=head2 accountline >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Accountline> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "accountline", >+ "Koha::Schema::Result::Accountline", >+ { accountlines_id => "accountlines_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+=head2 transaction >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::PaymentsTransaction> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "transaction", >+ "Koha::Schema::Result::PaymentsTransaction", >+ { transaction_id => "transaction_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+ >+# Created by DBIx::Class::Schema::Loader v0.07039 @ 2015-11-19 10:32:53 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ItfVA6ePztGiqVcJ/VPabQ >+ >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/api/v1/swagger.json b/api/v1/swagger.json >index 9c2f436..9fe90a8 100644 >--- a/api/v1/swagger.json >+++ b/api/v1/swagger.json >@@ -75,6 +75,76 @@ > } > } > } >+ }, >+ "/pos/cpu/{invoicenumber}": { >+ "get": { >+ "x-mojo-controller": "Koha::REST::V1::POSIntegration", >+ "operationId": "getTransaction", >+ "x-koha-permission": { >+ "updatecharges": "remaining_permissions" >+ }, >+ "tags": ["POS Integration"], >+ "parameters": [ >+ { >+ "$ref": "#/parameters/invoicenumberPathParam" >+ } >+ ], >+ "consumes": ["application/json"], >+ "produces": ["application/json"], >+ "responses": { >+ "200": { >+ "description": "A transaction", >+ "schema": { >+ "$ref" : "#/definitions/transaction" >+ } >+ }, >+ "404": { >+ "description": "Transaction not found", >+ "schema": { >+ "$ref": "#/definitions/error" >+ } >+ } >+ } >+ } >+ }, >+ "/pos/cpu/{invoicenumber}/report": { >+ "post": { >+ "x-mojo-controller": "Koha::REST::V1::POSIntegration", >+ "operationId": "cpuReport", >+ "tags": ["POS Integration"], >+ "parameters": [ >+ { >+ "$ref": "#/parameters/invoicenumberPathParam" >+ }, >+ { >+ "name": "body", >+ "in": "body", >+ "type": "string", >+ "description": "New report", >+ "schema": { "$ref": "#/definitions/CPUinvoiceReport" } >+ } >+ ], >+ "consumes": ["application/json"], >+ "produces": ["application/json"], >+ "responses": { >+ "200": { >+ "description": "Response for receiving report", >+ "type": "string" >+ }, >+ "400": { >+ "description": "Bad parameters", >+ "schema": { >+ "$ref": "#/definitions/error" >+ } >+ }, >+ "404": { >+ "description": "Transaction not found", >+ "schema": { >+ "$ref": "#/definitions/error" >+ } >+ } >+ } >+ } > } > }, > "definitions": { >@@ -353,6 +423,58 @@ > "borrowernumber": { > "description": "Patron internal identifier" > }, >+ "transaction": { >+ "type": "object", >+ "properties": { >+ "borrowernumber": { >+ "$ref": "#/definitions/borrowernumber" >+ }, >+ "accountlines_id": { >+ "description": "Reference to related accountlines row where accounttype is Pay. If null, transaction is incomplete. Else it is completed.", >+ "type": "integer" >+ }, >+ "status": { >+ "description": "Status of transaction", >+ "type": "string" >+ }, >+ "timestamp": { >+ "description": "Creation time", >+ "type": "string" >+ }, >+ "description": { >+ "type": "string" >+ }, >+ "price_in_cents": { >+ "description": "Total price of transaction", >+ "type": "integer" >+ } >+ } >+ }, >+ "CPUinvoiceReport": { >+ "type": "object", >+ "properties": { >+ "Source": { >+ "type": "string" >+ }, >+ "Id": { >+ "description": "Invoice identification number", >+ "type": "string" >+ }, >+ "Status": { >+ "description": "Status of payment", >+ "type": "integer" >+ }, >+ "Reference": { >+ "description": "Receipt number for successful payments", >+ "type": "string", >+ "required": false >+ }, >+ "Hash": { >+ "description": "Hash for response parameters", >+ "type": "string" >+ } >+ } >+ }, > "error": { > "type": "object", > "properties": { >@@ -370,6 +492,13 @@ > "description": "Internal patron identifier", > "required": true, > "type": "integer" >+ }, >+ "invoicenumberPathParam": { >+ "name": "invoicenumber", >+ "in": "path", >+ "description": "Internal invoice identifier", >+ "required": "true", >+ "type": "integer" > } > } > } >diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml >index 74a5720..c920215 100644 >--- a/etc/koha-conf.xml >+++ b/etc/koha-conf.xml >@@ -136,5 +136,23 @@ __PAZPAR2_TOGGLE_XML_POST__ > <font type="HBO">/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-BoldOblique.ttf</font> > </ttf> > >+ <pos> >+ <CPU> >+ <!-- Delivered by CPU: --> >+ <source></source> <!-- Source id --> >+ <secretKey></secretKey> <!-- Secret key for generating SHA-256 hash --> >+ <url></url> <!-- Address to the cash register server --> >+ >+ <!-- Koha settings --> >+ <mode></mode> <!-- Use 2 for synchronized mode --> >+ <notificationAddress></notificationAddress> <!-- https://server/api/v1/pos/cpu/{invoicenumber}/report --> >+ <!-- Replace "server" with your server address, but keep {invoicenumber} as it is (it will be converted later into real id) --> >+ >+ <!-- SSL certificates --> >+ <ssl_cert></ssl_cert> <!-- SSL certificate path --> >+ <ssl_key></ssl_key> <!-- SSL key path --> >+ <ssl_ca_file></ssl_ca_file> <!-- CA certificate path --> >+ </CPU> >+ </pos> > </config> > </yazgfs> >diff --git a/etc/koha-httpd.conf b/etc/koha-httpd.conf >index 32cdf0a..0884788 100644 >--- a/etc/koha-httpd.conf >+++ b/etc/koha-httpd.conf >@@ -37,6 +37,12 @@ > Deny from all > </DirectoryMatch> > >+# <LocationMatch "/api/v1/pos/cpu/\d+/report"> >+# Order Deny,Allow >+# Deny from all >+# Allow from 10.1.62.83 >+# </LocationMatch> >+ > <IfModule mod_gzip.c> > mod_gzip_on yes > mod_gzip_dechunk yes >@@ -170,6 +176,12 @@ > Deny from all > </DirectoryMatch> > >+# <LocationMatch "/api/v1/pos/cpu/\d+/report"> >+# Order Deny,Allow >+# Deny from all >+# Allow from 10.1.62.83 >+# </LocationMatch> >+ > <IfModule mod_gzip.c> > mod_gzip_on yes > mod_gzip_dechunk yes >diff --git a/installer/data/mysql/atomicupdate/KD#377-CPU_Integration-Add_table_for_transactions.pl b/installer/data/mysql/atomicupdate/KD#377-CPU_Integration-Add_table_for_transactions.pl >new file mode 100644 >index 0000000..d4a530f >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/KD#377-CPU_Integration-Add_table_for_transactions.pl >@@ -0,0 +1,51 @@ >+#! /usr/bin/perl >+ >+use strict; >+use warnings; >+use C4::Context; >+use Koha::AtomicUpdater; >+ >+my $dbh = C4::Context->dbh; >+my $atomicUpdater = Koha::AtomicUpdater->new(); >+ >+unless($atomicUpdater->find('KD#377')) { >+ $dbh->do(" >+ CREATE TABLE payments_transactions ( >+ transaction_id int(11) NOT NULL auto_increment, >+ borrowernumber int(11) NOT NULL, >+ accountlines_id int(11), >+ status ENUM('paid','pending','cancelled','unsent','processing') DEFAULT 'unsent', >+ timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, >+ description TEXT NOT NULL, >+ price_in_cents int(11) NOT NULL, >+ PRIMARY KEY (transaction_id), >+ FOREIGN KEY (accountlines_id) >+ REFERENCES accountlines(accountlines_id) >+ ON DELETE CASCADE, >+ FOREIGN KEY (borrowernumber) >+ REFERENCES borrowers(borrowernumber) >+ ON DELETE CASCADE >+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; >+ "); >+ $dbh->do(" >+ CREATE TABLE payments_transactions_accountlines ( >+ transactions_accountlines_id int(11) NOT NULL auto_increment, >+ transaction_id int(11) NOT NULL, >+ accountlines_id int(11) NOT NULL, >+ paid_price_cents int(11) NOT NULL, >+ PRIMARY KEY (transactions_accountlines_id), >+ FOREIGN KEY (transaction_id) >+ REFERENCES payments_transactions(transaction_id) >+ ON DELETE CASCADE, >+ FOREIGN KEY (accountlines_id) >+ REFERENCES accountlines(accountlines_id) >+ ON DELETE CASCADE >+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; >+ "); >+ >+ # Add system preferences >+ $dbh->do("INSERT INTO systempreferences (variable, value, options, explanation, type) VALUES ('cpuitemnumbers', '', '', 'Maps Koha account types into Ceepos items', 'textarea')"); >+ $dbh->do("INSERT INTO systempreferences (variable, value, options, explanation, type) VALUES ('POSIntegration', 'OFF', 'cpu|OFF', 'Selects used POS integration', 'choice')"); >+ >+ print "Upgrade to done (KD#377 CPU integration: Add table for transactions)\n"; >+} >\ No newline at end of file >diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql >index de53c0c..7144f89 100644 >--- a/installer/data/mysql/kohastructure.sql >+++ b/installer/data/mysql/kohastructure.sql >@@ -1751,6 +1751,47 @@ CREATE TABLE `patronimage` ( -- information related to patron images > CONSTRAINT `patronimage_fk1` FOREIGN KEY (`borrowernumber`) REFERENCES `borrowers` (`borrowernumber`) ON DELETE CASCADE ON UPDATE CASCADE > ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; > >+- >+-- Table structure for table `payments_transactions` >+-- >+ >+DROP TABLE IF EXISTS `payments_transactions`; >+CREATE TABLE `payments_transactions` ( -- information related to payments via POS integration >+ transaction_id int(11) NOT NULL auto_increment, -- transaction number >+ borrowernumber int(11) NOT NULL, -- the borrowernumber that the payment is for >+ accountlines_id int(11), -- the accountlines_id of the payment (the accounttype is Pay) >+ status ENUM('paid','pending','cancelled','unsent') DEFAULT 'pending', -- status of transaction >+ timestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- timestamp for payment initialization >+ description TEXT NOT NULL, -- additional description that can hold notes. Prints into the accountlines Pay event once the payment is completed >+ price_in_cents int(11) NOT NULL, -- total price of the payment in cents >+ PRIMARY KEY (transaction_id), >+ FOREIGN KEY (accountlines_id) >+ REFERENCES accountlines(accountlines_id) >+ ON DELETE CASCADE, >+ FOREIGN KEY (borrowernumber) >+ REFERENCES borrowers(borrowernumber) >+ ON DELETE CASCADE >+) ENGINE=InnoDB DEFAULT CHARSET=utf8; >+ >+-- >+-- Table structure for table `payments_transactions_accountlines` >+-- >+ >+DROP TABLE IF EXISTS `payments_transactions_accountlines`; >+CREATE TABLE `payments_transactions` ( -- related accountlines for payments (transactions) >+ transactions_accountlines_id int(11) NOT NULL auto_increment, >+ transaction_id int(11) NOT NULL, -- referenced transaction_id from payments_transactions >+ accountlines_id int(11) NOT NULL, -- referenced accountlines_id from accountlines >+ paid_price_cents int(11) NOT NULL, -- price (in cents) of the item in accountlines >+ PRIMARY KEY (transactions_accountlines_id), >+ FOREIGN KEY (transaction_id) >+ REFERENCES payments_transactions(transaction_id) >+ ON DELETE CASCADE, >+ FOREIGN KEY (accountlines_id) >+ REFERENCES accountlines(accountlines_id) >+ ON DELETE CASCADE >+) ENGINE=InnoDB DEFAULT CHARSET=utf8; >+ > -- Table structure for table `pending_offline_operations` > -- > -- this table is MyISAM, InnoDB tables are growing only and this table is filled/emptied/filled/emptied... >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/tools.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/tools.pref >index 5a3ddce..3989ea7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/tools.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/tools.pref >@@ -21,3 +21,17 @@ Tools: > staff: "Staff client only" > both: "Both OPAC and staff client" > - >+ Cash registers: >+ - >+ - Use >+ - pref: POSIntegration >+ choices: >+ cpu: CPU integration >+ "OFF": None >+ - component to handle Borrower's fine payments. >+ - >+ - CPU / Ceepos integration. Map accountlines' accounttypes to item numbers for cash registers in different branches. >+ - pref: cpuitemnumbers >+ type: textarea >+ class: code >+ - Use parameter "Default" to define an item number for other types than defined. >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt >index 824c41c..b5ef12b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt >@@ -32,6 +32,14 @@ $(document).ready(function() { > }); > }); > </script> >+<style type="text/css"> >+ td.transaction-selected { >+ background:#FFBC8F !important; >+ } >+ tr:hover td.transaction-selected { >+ background:#F0A16C !important; >+ } >+</style> > </head> > <body id="pat_borraccount" class="pat"> > [% INCLUDE 'header.inc' %] >@@ -61,6 +69,7 @@ $(document).ready(function() { > <thead> > <tr> > <th class="title-string">Date</th> >+ <th>Transaction number</th> > <th>Description of charges</th> > <th>Note</th> > <th>Amount</th> >@@ -75,8 +84,12 @@ $(document).ready(function() { > <!-- FIXME: Shouldn't hardcode dollar signs, since Euro or Pound might be needed --> > [% FOREACH account IN accounts %] > >- [% IF ( loop.odd ) %]<tr>[% ELSE %]<tr class="highlight">[% END %] >+ [% IF ( loop.odd ) %]<tr [% IF account.transactionnumber %]class="transaction"[% END %]>[% ELSE %]<tr class="highlight [% IF account.transactionnumber %]transaction[% END %]">[% END %] >+ [% FOREACH relline IN relatedaccounts.${account.accountlines_id} %] >+ <input type="hidden" name="parentTransaction" value="[% relline %]" /> >+ [% END %] > <td><span title="[% account.date %]">[% account.date |$KohaDates %]</span></td> >+ <td class="transactionnumber">[% IF account.transactionnumber %][% account.transactionnumber %][% ELSE %]-[% END %]</td> > <td> > [% SWITCH account.accounttype %] > [% CASE 'Pay' %]Payment, thanks >@@ -125,14 +138,14 @@ $(document).ready(function() { > [% END %] > <tfoot> > <tr> >- <td colspan="4">Total due</td> >+ <td colspan="5">Total due</td> > [% IF ( totalcredit ) %] > <td class="credit" style="text-align: right;">[% total %]</td> > [% ELSE %] > <td class="debit"style="text-align: right;">[% total %]</td> > [% END %] > [% IF ( reverse_col ) %] >- <td colspan="2"></td> >+ <td colspan="3"></td> > [% ELSE %] > <td></td> > [% END %] >@@ -143,7 +156,37 @@ $(document).ready(function() { > > </div> > </div> >+<script type="text/javascript"> >+ $("tr").on("click", function(e) { >+ $("td").each(function() { >+ if ($(this).hasClass("transaction-selected")) { >+ $(this).removeClass("transaction-selected"); >+ } >+ }); >+ if ($(this).hasClass("transaction")) { >+ $(this).children("td").addClass("transaction-selected"); >+ $(this).children("td").each(function() { >+ if ($(this).hasClass("transactionnumber")) { >+ var transactionnumber = $(this).html(); > >+ $("input[name*='parentTransaction']").each(function() { >+ if ($(this).val() == transactionnumber) { >+ $(this).parent().children("td").addClass("transaction-selected"); >+ } >+ }); >+ } >+ }); >+ } >+ e.stopPropagation(); >+ }) >+ $(document).click(function(e) { >+ $("td").each(function() { >+ if ($(this).hasClass("transaction-selected")) { >+ $(this).removeClass("transaction-selected"); >+ } >+ }); >+ }); >+</script> > <div class="yui-b"> > [% INCLUDE 'circ-menu.inc' %] > </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/paycollect.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/paycollect.tt >index cb6902c..0c9ffdb 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/paycollect.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/paycollect.tt >@@ -61,6 +61,102 @@ function moneyFormat(textObj) { > } > //]]> > </script> >+<style type="text/css"> >+ .ball { >+ background-color: rgba(0,0,0,0); >+ border: 25px solid rgba(240,192,71,0.9); >+ opacity: .9; >+ border-top: 25px solid rgba(0,0,0,0); >+ border-left: 25px solid rgba(0,0,0,0); >+ border-radius: 150px; >+ box-shadow: 0 0 35px #f0c047; >+ width: 140px; >+ height: 140px; >+ margin: 50px auto; >+ -moz-animation: spin 1s infinite linear; >+ -webkit-animation: spin 1s infinite linear; >+ } >+ >+ .ball1 { >+ background-color: rgba(0,0,0,0); >+ border: 25px solid rgba(240,192,71,0.9); >+ opacity: .9; >+ border-top: 25px solid rgba(0,0,0,0); >+ border-left: 25px solid rgba(0,0,0,0); >+ border-radius: 150px; >+ box-shadow: 0 0 15px #f0c047; >+ width: 70px; >+ height: 70px; >+ margin: 0 auto; >+ position: relative; >+ top: -205px; >+ -moz-animation: spinoff .5s infinite linear; >+ -webkit-animation: spinoff .5s infinite linear; >+ } >+ >+ @-moz-keyframes spin { >+ 0% { >+ -moz-transform: rotate(0deg); >+ } >+ >+ 100% { >+ -moz-transform: rotate(360deg); >+ }; >+ } >+ >+ @-moz-keyframes spinoff { >+ 0% { >+ -moz-transform: rotate(0deg); >+ } >+ >+ 100% { >+ -moz-transform: rotate(-360deg); >+ }; >+ } >+ >+ @-webkit-keyframes spin { >+ 0% { >+ -webkit-transform: rotate(0deg); >+ } >+ >+ 100% { >+ -webkit-transform: rotate(360deg); >+ }; >+ } >+ >+ @-webkit-keyframes spinoff { >+ 0% { >+ -webkit-transform: rotate(0deg); >+ } >+ >+ 100% { >+ -webkit-transform: rotate(-360deg); >+ }; >+ } >+ >+ .office-button { >+ background:#eee; >+ border:solid 2px rgba(240,192,71,1); >+ border-radius: 5px; >+ font:3em Verdana; >+ margin:10px; >+ min-width:100px; >+ height:100px; >+ outline:none; >+ box-shadow:0 0 2px rgba(240,192,71,1); >+ } >+ .selected { >+ background:#afa; >+ border:solid 4px rgba(50,2020,50,1); >+ box-shadow:0 0 2px rgba(50,2020,50,1); >+ } >+ .office-button::-moz-focus-inner { >+ border:0; >+ } >+ #add_new_office { >+ cursor:pointer; >+ } >+</style> > </head> > <body id="pat_paycollect" class="pat"> > [% INCLUDE 'header.inc' %] >@@ -98,8 +194,106 @@ function moneyFormat(textObj) { > </div> > [% END %] > >+[% IF ( startSending ) %] >+ <div><h2>Processing payment [% payment.Id %] - Please complete the payment [% IF ( payment.Office ) %]at cash register [% payment.Office %][% ELSE %] at any cash register[% END %].</h2> >+ <div class="ball"></div> >+ <div class="ball1"></div> >+ </div> >+ <div><p> >+ Current status: >+ <span id="status"> >+ <span class="connecting">Connecting to the cash register.</span> >+ <span class="pending" style="display:none">Payment is pending. Please complete the payment and navigate back to <a href="/cgi-bin/koha/members/boraccount.pl?borrowernumber=10902939">Account</a>.</span> >+ <span class="paid" style="display:none">Payment is completed.</span> >+ <span class="cancelled" style="display:none">Payment was cancelled.</span> >+ <span class="processing" style="display:none">Payment is completed, but Koha is still processing it.</span> >+ </span> >+ </p> >+ <button type="button" id="recheck">Update payment status</button> >+ </div> >+ <script type="text/javascript"> >+ $(document).ready(function() { >+ $.ajax({ >+ url: "/cgi-bin/koha/members/paycollect.pl", >+ type: "post", >+ dataType: "json", >+ data: JSON.stringify([% json_payment %]), >+ contentType: "application/json" >+ }).done(function(data){ >+ var response = jQuery.parseJSON(JSON.stringify(data)); >+ >+ handlePOSResponse(data); >+ }); >+ $("#recheck").click(function() { >+ $.get("/api/v1/pos/cpu/" + [% payment.Id %], function(data) { >+ var response = jQuery.parseJSON(JSON.stringify(data)); >+ >+ handlePOSResponse(response); >+ }); >+ }); >+ }); >+ >+ function handlePOSResponse(response){ >+ $("span[id='status']").children().css("display","none"); >+ if (response.Status == 0 || response.status == "cancelled") { >+ $("span.cancelled").css("display", "inline-block"); >+ colorBall("red"); >+ >+ if (response.error) { >+ alert(_("Error: ") + JSON.stringify(response)); >+ } >+ >+ window.location.replace("/cgi-bin/koha/members/boraccount.pl?borrowernumber=[% borrowernumber %]"); >+ } >+ else if (response.Status == 1 || response.status == "paid") { >+ $("span.paid").css("display", "inline-block"); >+ colorBall("green"); >+ window.location.replace("/cgi-bin/koha/members/boraccount.pl?borrowernumber=[% borrowernumber %]"); >+ } >+ else if (response.Status == 2 || response.status == "pending") { >+ $("span.pending").css("display", "inline-block"); >+ } >+ else if (response.status == "processing") { >+ $("span.processing").css("display", "inline-block"); >+ } >+ else if (response.Status == 97) { >+ $("span.cancelled").css("display", "inline-block"); >+ colorBall("red"); >+ alert(_("Duplicate transaction number: ") + JSON.stringify(response)); >+ } >+ else if (response.Status == 98) { >+ $("span.cancelled").css("display", "inline-block"); >+ colorBall("red"); >+ alert(_("Cash register server system error: ") + JSON.stringify(response)); >+ } >+ else if (response.Status == 99) { >+ $("span.cancelled").css("display", "inline-block"); >+ colorBall("red"); >+ alert(_("Payment request is malformed: ") + JSON.stringify(response)); >+ } >+ else { >+ $("span.cancelled").css("display", "inline-block"); >+ alert(_("Error: ") + JSON.stringify(response)); >+ } >+ } >+ >+ function colorBall(color) { >+ if (color == "red"){ >+ $(".ball").css("border", "25px solid rgba(204,0,0,0.9)"); >+ $(".ball1").css("border", "25px solid rgba(204,0,0,0.9)"); >+ $(".ball").css("box-shadow", "0 0 35px rgb(204,0,0)"); >+ $(".ball1").css("box-shadow", "0 0 15px rgb(204,0,0)"); >+ } else if (color == "green") { >+ $(".ball").css("border", "25px solid rgba(0,204,0,0.9)"); >+ $(".ball1").css("border", "25px solid rgba(0,204,0,0.9)"); >+ $(".ball").css("box-shadow", "0 0 35px rgb(0,204,0)"); >+ $(".ball1").css("box-shadow", "0 0 15px rgb(0,204,0)"); >+ } >+ } >+ </script> >+[% ELSE %] > [% IF ( pay_individual ) %] >- <form name="payindivfine" id="payindivfine" onsubmit="return validatePayment(this);" method="post" action="/cgi-bin/koha/members/paycollect.pl"> >+ <form name="payindivfine" id="payindivfine" onsubmit="return validatePayment(this)" method="post" action="/cgi-bin/koha/members/paycollect.pl"> > <input type="hidden" name="borrowernumber" id="borrowernumber" value="[% borrower.borrowernumber %]" /> > <input type="hidden" name="pay_individual" id="pay_individual" value="[% pay_individual %]" /> > <input type="hidden" name="itemnumber" id="itemnumber" value="[% itemnumber %]" /> >@@ -147,7 +341,11 @@ function moneyFormat(textObj) { > <!-- default to paying all --> > <input name="paid" id="paid" value="[% amountoutstanding | format('%.2f') %]" onchange="moneyFormat(document.payindivfine.paid)"/> > </li> >+ <li> >+ [% INCLUDE offices %] >+ </li> > </ol> >+<div> > </fieldset> > > <div class="action"><input type="submit" name="submitbutton" value="Confirm" /> >@@ -216,12 +414,16 @@ function moneyFormat(textObj) { > <label for="selected_accts_notes">Note: </label> > <textarea name="selected_accts_notes" id="selected_accts_notes">[% selected_accts_notes %]</textarea> > </li> >+ <li> >+ [% INCLUDE offices %] >+ </li> > </ol> > </fieldset> > <div class="action"><input type="submit" name="submitbutton" value="Confirm" /> > <a class="cancel" href="/cgi-bin/koha/members/boraccount.pl?borrowernumber=[% borrower.borrowernumber %]">Cancel</a></div> > </form> > [% END %] >+[% END %] > </div></div> > </div> > </div> >@@ -231,4 +433,161 @@ function moneyFormat(textObj) { > </div> > </div> > [% INCLUDE 'intranet-bottom.inc' %] >+[% BLOCK offices %] >+ [% IF POSIntegration %] >+ [% IF POSIntegration_in_branch %] >+ <label for="offices">Select office:</label> >+ <div id="offices"></div> >+ <input type="hidden" name="Office" id="office" /> >+ <span id="add_new_office"><a>Add new office</a></span> >+ <div id="office_form" style="display:none"> >+ <label for="office_new">Office:</label><input type="text" name="office_new" id="office_new" /> >+ <button id="new_office" type="button">Add new office</button> >+ <p>Offices are stored as a browser cookie. Currently stored offices will disappear and need to be added again if you clear the browser cookies.</p> >+ <p>You can delete a stored office by first selecting it, and then pressing the delete-key on your keyboard.</p> >+ </div> >+ [% ELSE %] >+ <span id="add_new_office"><a>Add new office</a></span> >+ <div id="office_form" style="display:none"> >+ <p>Cash register integration has not been enabled for your branch. In order to enable cash register integration in this branch ([% branch %]), please contact a superlibrarian. Koha payment types need to be mapped into your cash register item numbers.</p> >+ </div> >+ [% END%] >+ [% END %] >+[% END %] >+[% IF POSIntegration %] >+<script type="text/javascript"> >+ $(document).ready(function() { >+ $("#add_new_office").click(function() { >+ if ($("#office_form").css("display") == "none") { >+ $("#office_form").css("display", "block"); >+ } else { >+ $("#office_form").css("display", "none"); >+ } >+ }); >+ [% IF POSIntegration_in_branch %] >+ loadCashRegisters(); >+ $("#new_office").click(function() { >+ if ($("#office_new").val().length > 0) { >+ newOffice($("#office_new").val()); >+ } >+ }); >+ >+ $("#office").bind("enterKey",function(e) { >+ if ($("#office_new").val().length > 0) { >+ newOffice($("#office_new").val()); >+ } >+ }); >+ $("#office_new").keyup(function(e) { >+ if(e.keyCode == 13) { >+ $(this).trigger("enterKey"); >+ } >+ }); >+ >+ $("body").on("click", "button[id*=office-]", function() { >+ selectOffice(this); >+ }); >+ $("body").on("keyup", "button[id*=office-]", function(e) { >+ if(e.keyCode == 46) { >+ deleteOffice(this); >+ } >+ }); >+ [% END %] >+ }); >+ [% IF POSIntegration_in_branch %] >+ function selectOffice(button) { >+ // Deselect all other buttons >+ $.each($("button[id*=office-]"), function(key, obj) { >+ $(obj).attr("class", "office-button"); >+ }) >+ $(button).attr("class", "office-button selected"); >+ >+ $("#office").val($(button).attr("id").substr(7)); >+ } >+ function newOffice(office) { >+ var offices = getOffices(); >+ >+ if (offices == null) { >+ offices = []; >+ } >+ if (offices.indexOf(office) > -1) { >+ return offices; >+ } >+ offices.push(office); > >+ $.each(offices, function(key, val){ >+ if (!$("#office-"+val).length) { >+ $("#offices").append('<button id="office-'+val+'" type="button" class="office-button">'+val+'</button>'); >+ } >+ }); >+ >+ setCookie("offices", JSON.stringify(offices), "100*365"); >+ >+ $("#office_new").val(""); >+ return offices; >+ } >+ function deleteOffice(obj) { >+ var offices = getOffices(); >+ >+ if (offices == null) { >+ return null; >+ } >+ >+ $(obj).remove(); >+ >+ var index = offices.indexOf($(obj).attr("id").substr(7)); >+ >+ offices.splice(index, 1); >+ >+ if ($("#office").val() == $(obj).attr("id").substr(7)) { >+ $("#office").val(""); >+ } >+ setCookie("offices", JSON.stringify(offices), "100*365"); >+ >+ return offices; >+ } >+ function loadCashRegisters() { >+ var offices = getOffices(); >+ >+ if (offices == null) { >+ return null; >+ } >+ $.each(offices, function(key, val){ >+ if (!$("#office-"+val).length) { >+ $("#offices").append('<button id="office-'+val+'" type="button" class="office-button">'+val+'</button>'); >+ } >+ }); >+ } >+ function getOffices() { >+ var cookie; >+ var name = "offices="; >+ var ca = document.cookie.split(';'); >+ for(var i=0; i<ca.length; i++) { >+ var c = ca[i]; >+ while (c.charAt(0)==' ') c = c.substring(1); >+ if (c.indexOf(name) == 0) cookie = c.substring(name.length,c.length); >+ } >+ >+ if (!cookie) { >+ return null; >+ } >+ >+ return JSON.parse(cookie); >+ } >+ >+ function setCookie(cname, cvalue, exdays) { >+ var d = new Date(); >+ d.setTime(d.getTime() + (exdays*24*60*60*1000)); >+ var expires = "expires="+d.toUTCString(); >+ document.cookie = cname + "=" + cvalue + "; " + expires; >+ } >+ >+ function validatePayment(obj) { >+ if ($("#office").val().length == 0) { >+ alert(_("Please select office")); >+ return false; >+ } >+ $(obj)[0].submit(); >+ } >+ [% END %] >+</script> >+[% END %] >diff --git a/members/boraccount.pl b/members/boraccount.pl >index 0200c5c..463f5c0 100755 >--- a/members/boraccount.pl >+++ b/members/boraccount.pl >@@ -33,6 +33,9 @@ use C4::Branch; > use C4::Accounts; > use C4::Members::Attributes qw(GetBorrowerAttributes); > >+use Koha::PaymentsTransaction; >+use Koha::PaymentsTransactions; >+ > my $input=new CGI; > > >@@ -71,6 +74,7 @@ my $totalcredit; > if($total <= 0){ > $totalcredit = 1; > } >+my $related_accountlines; > > my $reverse_col = 0; # Flag whether we need to show the reverse column > foreach my $accountline ( @{$accts}) { >@@ -89,6 +93,21 @@ foreach my $accountline ( @{$accts}) { > $accountline->{payment} = 1; > $reverse_col = 1; > } >+ >+ my $transaction = Koha::PaymentsTransactions->find({ accountlines_id => $accountline->{accountlines_id} }) if $accountline->{accounttype} eq "Pay"; >+ >+ # If transaction is found, find all related accountlines and store them so we can highlight >+ # them in the Fines tab. >+ if ($transaction) { >+ $accountline->{transactionnumber} = $transaction->transaction_id if $transaction; >+ >+ my $relacclines = $transaction->GetRelatedAccountlines(); >+ foreach my $relaccline (@$relacclines){ >+ $related_accountlines->{$relaccline->{accountlines_id}} = [] if not exists $related_accountlines->{$relaccline->{accountlines_id}}; >+ >+ push $related_accountlines->{$relaccline->{accountlines_id}}, $transaction->transaction_id; >+ } >+ } > } > > $template->param( adultborrower => 1 ) if ( $data->{'category_type'} eq 'A' ); >@@ -115,6 +134,7 @@ $template->param( > is_child => ($data->{'category_type'} eq 'C'), > reverse_col => $reverse_col, > accounts => $accts, >+ relatedaccounts => $related_accountlines, > activeBorrowerRelationship => (C4::Context->preference('borrowerRelationship') ne ''), > RoutingSerials => C4::Context->preference('RoutingSerials'), > ); >diff --git a/members/paycollect.pl b/members/paycollect.pl >index 7254a3a..4ded4ff 100755 >--- a/members/paycollect.pl >+++ b/members/paycollect.pl >@@ -29,6 +29,8 @@ use C4::Members::Attributes qw(GetBorrowerAttributes); > use C4::Accounts; > use C4::Koha; > use C4::Branch; >+use C4::OPLIB::CPUIntegration; >+use JSON; > > my $input = CGI->new(); > >@@ -43,6 +45,18 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( > } > ); > >+# POS integration AJAX call >+my $posintegration = 1 if (C4::Context->preference("POSIntegration") ne "OFF"); >+my $posintegration_in_branch = 1 if C4::OPLIB::CPUIntegration::hasBranchEnabledIntegration(C4::Branch::mybranch()); >+if ($posintegration && $posintegration_in_branch && $input->param('POSTDATA')) { >+ my $payment = JSON->new->utf8->canonical(1)->decode($input->param('POSTDATA')); >+ >+ if ($payment->{send_payment} && $payment->{send_payment} eq "POST") { >+ output_ajax_with_http_headers $input, C4::OPLIB::CPUIntegration::SendPayment($payment); >+ exit 1; >+ } >+} >+ > # get borrower details > my $borrowernumber = $input->param('borrowernumber'); > my $borrower = GetMember( borrowernumber => $borrowernumber ); >@@ -58,9 +72,14 @@ my $individual = $input->param('pay_individual'); > my $writeoff = $input->param('writeoff_individual'); > my $select_lines = $input->param('selected'); > my $select = $input->param('selected_accts'); >+my $office = $input->param('Office'); > my $payment_note = uri_unescape $input->param('payment_note'); > my $accountno; > my $accountlines_id; >+ >+$template->param( POSIntegration => 1 ) if $posintegration; >+$template->param( POSIntegration_in_branch => 1 ) if $posintegration_in_branch; >+ > if ( $individual || $writeoff ) { > if ($individual) { > $template->param( pay_individual => 1 ); >@@ -107,6 +126,28 @@ if ( $total_paid and $total_paid ne '0.00' ) { > total_due => $total_due > ); > } else { >+ if ($posintegration and C4::Context->preference("POSIntegration") eq "cpu" and $posintegration_in_branch) { >+ my $payment; >+ >+ $payment->{borrowernumber} = $borrowernumber; >+ $payment->{total_paid} = $total_paid; >+ $payment->{total_due} = $total_due; >+ $payment->{payment_note} = $payment_note || $input->param('notes') || $input->param('selected_accts_notes'); >+ $payment->{office} = $office; >+ my @selected = (defined $select) ? split /,/, $select : $accountlines_id; >+ $payment->{selected} = \@selected; >+ >+ my $CPUPayment = C4::OPLIB::CPUIntegration::InitializePayment($payment); >+ >+ $template->param( >+ startSending => 1, >+ payment => $CPUPayment, >+ posdestination => C4::Context->config('pos')->{'CPU'}->{'url'}, >+ json_payment => JSON::encode_json($CPUPayment), >+ office => $office, >+ ); >+ } else { >+ > if ($individual) { > if ( $total_paid == $total_due ) { > makepayment( $accountlines_id, $borrowernumber, $accountno, $total_paid, $user, >@@ -136,6 +177,8 @@ if ( $total_paid and $total_paid ne '0.00' ) { > "/cgi-bin/koha/members/boraccount.pl?borrowernumber=$borrowernumber" > ); > } >+ >+ } > } > } else { > $total_paid = '0.00'; #TODO not right with pay_individual >@@ -148,6 +191,7 @@ $template->param(%$borrower); > $template->param( > borrowernumber => $borrowernumber, # some templates require global > borrower => $borrower, >+ branch => C4::Branch::mybranch(), > total => $total_due, > activeBorrowerRelationship => (C4::Context->preference('borrowerRelationship') ne ''), > RoutingSerials => C4::Context->preference('RoutingSerials'), >diff --git a/t/db_dependent/CPUIntegration.t b/t/db_dependent/CPUIntegration.t >new file mode 100644 >index 0000000..7ed02c0 >--- /dev/null >+++ b/t/db_dependent/CPUIntegration.t >@@ -0,0 +1,208 @@ >+#!/usr/bin/env perl >+ >+# Copyright 2015 Open Source Freedom Fighters >+# >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+$ENV{KOHA_PAGEOBJECT_DEBUG} = 1; >+use Modern::Perl; >+ >+use Test::More; >+use Try::Tiny; #Even Selenium::Remote::Driver uses Try::Tiny :) >+ >+use Koha::Auth::PermissionManager; >+use Koha::PaymentsTransaction; >+use Koha::PaymentsTransactions; >+ >+use t::lib::Page::Mainpage; >+use t::lib::Page::Members::Boraccount; >+use t::lib::Page::Members::Pay; >+use t::lib::Page::Members::Paycollect; >+ >+use t::lib::TestObjects::BorrowerFactory; >+use t::lib::TestObjects::SystemPreferenceFactory; >+use t::lib::TestObjects::FinesFactory; >+ >+use bignum; >+ >+##Setting up the test context >+my $testContext = {}; >+ >+my $password = '1234'; >+my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new(); >+my $borrowers = $borrowerFactory->createTestGroup([ >+ {firstname => 'Testthree', >+ surname => 'Testfour', >+ cardnumber => 'superuberadmin', >+ branchcode => 'CPL', >+ userid => 'god', >+ address => 'testi', >+ city => 'joensuu', >+ zipcode => '80100', >+ password => $password, >+ }, >+ {firstname => 'Iral', >+ surname => 'Aluksat', >+ cardnumber => 'superuberadmin2', >+ branchcode => 'CPL', >+ userid => 'god2', >+ address => 'testi', >+ city => 'joensuu', >+ zipcode => '80100', >+ password => $password, >+ }, >+ ], undef, $testContext); >+ >+my $systempreferences = t::lib::TestObjects::SystemPreferenceFactory->createTestGroup([ >+ {preference => 'POSIntegration', >+ value => 'cpu', >+ }, >+ {preference => 'cpuitemnumbers', >+ value => ' >+ CPL: >+ Default: 0000 >+ ', >+ }, >+ ], undef, $testContext); >+ >+my $fines = t::lib::TestObjects::FinesFactory->createTestGroup([ >+ { >+ note => "First", >+ cardnumber => $borrowers->{'superuberadmin'}->cardnumber, >+ amount => int(rand(9)+1) . "" . int(rand(10)) . "." . int(rand(10)) . "" . int(rand(10)) >+ }, >+ { >+ note => "Second", >+ cardnumber => $borrowers->{'superuberadmin'}->cardnumber, >+ amount => int(rand(9)+1) . "" . int(rand(10)) . "." . int(rand(10)) . "" . int(rand(10)) >+ }, >+ { >+ note => "First2", >+ cardnumber => $borrowers->{'superuberadmin2'}->cardnumber, >+ amount => int(rand(9)+1) . "" . int(rand(10)) . "." . int(rand(10)) . "" . int(rand(10)) >+ }, >+ { >+ note => "Second2", >+ cardnumber => $borrowers->{'superuberadmin2'}->cardnumber, >+ amount => int(rand(9)+1) . "" . int(rand(10)) . "." . int(rand(10)) . "" . int(rand(10)) >+ }, >+], undef, $testContext); >+ >+my $permissionManager = Koha::Auth::PermissionManager->new(); >+$permissionManager->grantPermissions($borrowers->{'superuberadmin'}, {superlibrarian => 'superlibrarian'}); >+$permissionManager->grantPermissions($borrowers->{'superuberadmin2'}, {superlibrarian => 'superlibrarian'}); >+eval { >+ MakeFullPayment($fines); >+ MakePartialPayment($fines); >+}; >+if ($@) { #Catch all leaking errors and gracefully terminate. >+ warn $@; >+ tearDown(); >+ exit 1; >+} >+ >+##All tests done, tear down test context >+tearDown(); >+done_testing; >+ >+sub tearDown { >+ t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); >+} >+ >+ >+ >+sub MakeFullPayment { >+ my ($fines) = @_; >+ # Make random amount for payments >+ my $firstAmount = $fines->{"First"}->{amount}; >+ my $secondAmount = $fines->{"Second"}->{amount}; >+ >+ # staff client >+ my $boraccount = t::lib::Page::Members::Boraccount->new({borrowernumber => $borrowers->{'superuberadmin'}->borrowernumber, op => 'modify', destination => 'circ', categorycode => 'PT'}); >+ >+ $boraccount = $boraccount->doPasswordLogin($borrowers->{'superuberadmin'}->userid(), $password) >+ ->findFine("First") # find the two fines created... >+ ->findFine("Second") # ...by FinesFactory >+ ->isFineAmountOutstanding("First", $firstAmount) >+ ->isFineAmountOutstanding("Second", $secondAmount) >+ ->navigateToPayFinesTab() >+ ->PaySelected() >+ ->addNoteToSelected("Transaction that pays everything ;)") >+ ->openAddNewCashRegister() >+ ->addNewCashRegister(100) # add cash register number 100 >+ ->selectCashRegister(100) # and select it >+ ->sendPaymentToPOS() >+ ->paymentLoadingScreen() >+ ->waitUntilPaymentIsAcceptedAtPOS(); >+ >+ # Get transaction ids >+ my $transactions = Koha::PaymentsTransactions->find({ borrowernumber => $borrowers->{'superuberadmin'}->borrowernumber }); >+ >+ # Check that there is a transaction completed >+ foreach my $transaction ($transactions){ >+ $boraccount = $boraccount->isTransactionComplete($transaction->transaction_id); >+ $boraccount >+ ->isFinePaid("Transaction that pays everything ;)") # note of transaction >+ ->isFineAmount("Transaction that pays everything ;)", "-".sprintf("%.2f",$firstAmount+$secondAmount)); >+ } >+ $boraccount >+ ->isFineAmount("First", $firstAmount) >+ ->isFineAmount("Second", $secondAmount) >+ ->isFinePaid("First") # Make sure fines are paid >+ ->isFinePaid("Second"); # Also the second :) >+} >+ >+sub MakePartialPayment { >+ my ($fines) = @_; >+ # Make random amount for payments >+ my $firstAmount = $fines->{"First2"}->{amount}; >+ my $secondAmount = $fines->{"Second2"}->{amount}; >+ >+ my $partialPayment = $firstAmount-(int(rand(9)+1) . "." . int(rand(10)) . "" . int(rand(10))); >+ # staff client >+ my $boraccount = t::lib::Page::Members::Boraccount->new({borrowernumber => $borrowers->{'superuberadmin2'}->borrowernumber, op => 'modify', destination => 'circ', categorycode => 'PT'}); >+ >+ $boraccount = $boraccount->doPasswordLogin($borrowers->{'superuberadmin2'}->userid(), $password) >+ ->findFine("First2") # find the two fines created... >+ ->findFine("Second2") # ...by FinesFactory >+ ->isFineAmountOutstanding("First2", $firstAmount) >+ ->isFineAmountOutstanding("Second2", $secondAmount) >+ ->navigateToPayFinesTab() >+ ->PaySelected() >+ ->setAmount($partialPayment) >+ ->addNoteToSelected("Transaction that pays everything ;)2") >+ ->openAddNewCashRegister() >+ ->addNewCashRegister(100) # add cash register number 100 >+ ->selectCashRegister(100) # and select it >+ ->sendPaymentToPOS() >+ ->paymentLoadingScreen() >+ ->waitUntilPaymentIsAcceptedAtPOS(); >+ >+ # Get transaction ids >+ my $transactions = Koha::PaymentsTransactions->find({ borrowernumber => $borrowers->{'superuberadmin2'}->borrowernumber }); >+ >+ # Check that there is a transaction completed >+ foreach my $transaction ($transactions){ >+ $boraccount = $boraccount->isTransactionComplete($transaction->transaction_id); >+ $boraccount >+ ->isFinePaid("Transaction that pays everything ;)2") # note of transaction >+ ->isFineAmount("Transaction that pays everything ;)2", "-".(sprintf("%.2f",$partialPayment))); >+ } >+ $boraccount >+ ->isFineAmount("First2", $firstAmount) >+ ->isFineAmount("Second2", $secondAmount) >+ ->isFineAmountOutstanding("First2", sprintf("%.2f",$firstAmount-$partialPayment)) >+ ->isFineAmountOutstanding("Second2", $secondAmount); >+} >\ No newline at end of file >-- >1.9.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 15654
: 47256