Bug 19166: Add the ability to add adjustments to an invoice
authorNick Clemens <nick@bywatersolutions.com>
Wed, 23 Aug 2017 16:13:39 +0000 (16:13 +0000)
committerNick Clemens <nick@bywatersolutions.com>
Thu, 19 Jul 2018 17:28:35 +0000 (17:28 +0000)
This patchset adds the ability to add adjustments to an invoice, one can
provide a reason, an adjustment amount, select a budget, and choose
whether to encumber the funds before the invoice is closed or not

To test:

1 - Create a new invoice with or without a shipping cost
2 - Note there are no existing adjustments
3 - Add an adjustment
4 - Submit the form withno changes, nothing happens
5 - Update the adjustment you created, ensure changes are saved but no
extra adjustment created
6 - Add another invoice prodiving only reason or amount (you can have 0
        value adjustments)
7 - Verify the adjustment total at bottom is correct
8 - Recieve some orders
9 - Verify totals are correct

Signed-off-by: Séverine QUEUNE <severine.queune@bulac.fr>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
C4/Budgets.pm
Koha/Acquisition/Invoice/Adjustment.pm [new file with mode: 0644]
Koha/Acquisition/Invoice/Adjustments.pm [new file with mode: 0644]
acqui/invoice.pl
acqui/ordered.pl
acqui/spent.pl
koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice.tt
koha-tmpl/intranet-tmpl/prog/en/modules/acqui/ordered.tt
koha-tmpl/intranet-tmpl/prog/en/modules/acqui/spent.tt
t/db_dependent/Koha/Acquisition/Invoice/Adjustments.t [new file with mode: 0644]

index 644c5e5..a2a1a92 100644 (file)
@@ -22,6 +22,7 @@ use strict;
 use C4::Context;
 use Koha::Database;
 use Koha::Patrons;
+use Koha::InvoiceAdjustments;
 use C4::Debug;
 use vars qw(@ISA @EXPORT);
 
@@ -349,6 +350,11 @@ sub GetBudgetSpent {
     my ($shipmentcost_sum) = $sth->fetchrow_array;
     $sum += $shipmentcost_sum;
 
+    my $adjustments = Koha::InvoiceAdjustments->search({budget_id => $budget_id, closedate => { '!=' => undef } },{ join => 'invoiceid' });
+    while ( my $adj = $adjustments->next ){
+        $sum += $adj->adjustment;
+    }
+
        return $sum;
 }
 
@@ -365,6 +371,21 @@ sub GetBudgetOrdered {
        $sth->execute($budget_id);
        my $sum =  $sth->fetchrow_array;
 
+    $sth = $dbh->prepare(qq|
+        SELECT SUM(shipmentcost) AS sum
+        FROM aqinvoices
+        WHERE shipmentcost_budgetid = ?
+          AND closedate IS NULL
+    |);
+    $sth->execute($budget_id);
+    my ($shipmentcost_sum) = $sth->fetchrow_array;
+    $sum += $shipmentcost_sum;
+
+    my $adjustments = Koha::InvoiceAdjustments->search({budget_id => $budget_id, encumber_open => 1, closedate => undef},{ join => 'invoiceid' });
+    while ( my $adj = $adjustments->next ){
+        $sum += $adj->adjustment;
+    }
+
        return $sum;
 }
 
diff --git a/Koha/Acquisition/Invoice/Adjustment.pm b/Koha/Acquisition/Invoice/Adjustment.pm
new file mode 100644 (file)
index 0000000..e3d0bc2
--- /dev/null
@@ -0,0 +1,44 @@
+package Koha::Acquisition::Invoice::Adjustment;
+
+# 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 Carp;
+
+use Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::Acquisition::Invoice::Adjustment - Koha Invoice Adjustment class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'AqinvoiceAdjustment';
+}
+
+1;
diff --git a/Koha/Acquisition/Invoice/Adjustments.pm b/Koha/Acquisition/Invoice/Adjustments.pm
new file mode 100644 (file)
index 0000000..80dc1e8
--- /dev/null
@@ -0,0 +1,51 @@
+package Koha::Acquisition::Invoice::Adjustments;
+
+# 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::Acquisition::Invoice::Adjustment;
+
+use Carp;
+
+use Koha::Database;
+
+use Koha::Rating;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::Acquisition::Invoice::Adjustments - Koha Invoice Adjustments Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'AqinvoiceAdjustment';
+}
+
+sub object_class {
+    return 'Koha::Acquisition::Invoice::Adjustment';
+}
+
+1;
index 0f2933a..75b6091 100755 (executable)
@@ -38,6 +38,7 @@ use Koha::Acquisition::Booksellers;
 use Koha::Acquisition::Currencies;
 use Koha::DateUtils;
 use Koha::Misc::Files;
+use Koha::InvoiceAdjustments;
 
 my $input = new CGI;
 my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
@@ -108,7 +109,47 @@ elsif ( $op && $op eq 'delete' ) {
         exit 0;
     }
 }
+elsif ( $op && $op eq 'del_adj' ) {
+    my $adjustment_id  = $input->param('adjustment_id');
+    my $del_adj = Koha::InvoiceAdjustments->find( $adjustment_id );
+    $del_adj->delete() if ($del_adj);
+}
+elsif ( $op && $op eq 'mod_adj' ) {
+    my @adjustment_id  = $input->multi_param('adjustment_id');
+    my @adjustment     = $input->multi_param('adjustment');
+    my @reason         = $input->multi_param('reason');
+    my @note           = $input->multi_param('note');
+    my @budget_id      = $input->multi_param('budget_id');
+    my @encumber_open  = $input->multi_param('encumber_open');
+    my %e_open = map { $_ => 1 } @encumber_open;
 
+    for( my $i=0; $i < scalar @adjustment; $i++ ){
+        if( $adjustment_id[$i] eq 'new' ){
+            next unless ( $adjustment[$i] || $reason[$i] );
+            my $new_adj = Koha::InvoiceAdjustment->new({
+                invoiceid => $invoiceid,
+                adjustment => $adjustment[$i],
+                reason => $reason[$i],
+                note => $note[$i],
+                budget_id => $budget_id[$i] || undef,
+                encumber_open => $encumber_open[$i],
+            });
+            $new_adj->store();
+        }
+        else {
+            my $old_adj = Koha::InvoiceAdjustments->find( $adjustment_id[$i] );
+            unless ( $old_adj->adjustment == $adjustment[$i] && $old_adj->reason eq $reason[$i] && $old_adj->budget_id == $budget_id[$i] && $old_adj->encumber_open == $e_open{$adjustment_id[$i]} && $old_adj->note eq $note[$i] ){
+                $old_adj->timestamp(undef);
+                $old_adj->adjustment( $adjustment[$i] );
+                $old_adj->reason( $reason[$i] );
+                $old_adj->note( $note[$i] );
+                $old_adj->budget_id( $budget_id[$i] || undef );
+                $old_adj->encumber_open( $e_open{$adjustment_id[$i]} ? 1 : 0 );
+                $old_adj->update();
+            }
+        }
+    }
+}
 
 my $details = GetInvoiceDetails($invoiceid);
 my $bookseller = Koha::Acquisition::Booksellers->find( $details->{booksellerid} );
@@ -159,6 +200,9 @@ foreach my $budget (@$budgets) {
     push @budgets_loop, \%line;
 }
 
+my $adjustments = Koha::InvoiceAdjustments->search({ invoiceid => $details->{'invoiceid'} });
+if ( $adjustments ) { $template->param( adjustments => $adjustments ); }
+
 $template->param(
     invoiceid        => $details->{'invoiceid'},
     invoicenumber    => $details->{'invoicenumber'},
index a976cc9..c78ac66 100755 (executable)
@@ -32,6 +32,7 @@ use Modern::Perl;
 use CGI qw ( -utf8 );
 use C4::Auth;
 use C4::Output;
+use Koha::InvoiceAdjustments;
 
 my $dbh     = C4::Context->dbh;
 my $input   = new CGI;
@@ -95,12 +96,19 @@ while ( my $data = $sth->fetchrow_hashref ) {
         $total += $subtotal;
     }
 }
+
+my $adjustments = Koha::InvoiceAdjustments->search({budget_id => $fund_id, closedate => undef, encumber_open => 1 }, { join => 'invoiceid' } );
+while ( my $adj = $adjustments->next ){
+    $total += $adj->adjustment;
+}
+
 $total = sprintf( "%.2f", $total );
 
 $template->{VARS}->{'fund'}    = $fund_id;
 $template->{VARS}->{'ordered'} = \@ordered;
 $template->{VARS}->{'total'}   = $total;
 $template->{VARS}->{'fund_code'} = $fund_code;
+$template->{VARS}->{'adjustments'} = $adjustments;
 
 $sth->finish;
 
index 9a1c805..dfc5f85 100755 (executable)
@@ -34,6 +34,7 @@ use C4::Auth;
 use C4::Output;
 use Modern::Perl;
 use CGI qw ( -utf8 );
+use Koha::InvoiceAdjustments;
 
 my $dbh      = C4::Context->dbh;
 my $input    = new CGI;
@@ -118,6 +119,11 @@ while (my $data = $sth->fetchrow_hashref) {
 }
 $sth->finish;
 
+my $adjustments = Koha::InvoiceAdjustments->search({budget_id => $bookfund, closedate => { '!=' => undef } }, { join => 'invoiceid' } );
+while ( my $adj = $adjustments->next ){
+    $total += $adj->adjustment;
+}
+
 $total = sprintf( "%.2f", $total );
 
 $template->param(
@@ -125,6 +131,7 @@ $template->param(
     spent => \@spent,
     subtotal => $subtotal,
     shipmentcosts => \@shipmentcosts,
+    adjustments => $adjustments,
     total => $total,
     fund_code => $fund_code
 );
index ef9ebfd..25f5a14 100644 (file)
@@ -3,6 +3,8 @@
 [% USE KohaDates %]
 [% USE Price %]
 [% SET footerjs = 1 %]
+[% USE AuthorisedValues %]
+
 [% INCLUDE 'doc-head-open.inc' %]
 <title>Koha &rsaquo; Acquisitions &rsaquo; Invoice</title>
 [% Asset.css("css/datatables.css") %]
             [% END %]
         </fieldset>
       </form>
+      <form action="/cgi-bin/koha/acqui/invoice.pl" method="post" class="validated">
+          <input type="hidden" name="invoiceid" value="[% invoiceid %]" />
+              <table id="invoice_adj_table">
+                  <tr>
+                     <th>Id</th>
+                     <th>Amount</th>
+                     <th>Reason</th>
+                     <th>Note</th>
+                     <th>Fund</th>
+                     <th>Encumber while invoice open</th>
+                     <th>&nbsp</th>
+                  </tr>
+                  [% IF (adjustments.count > 0) %]
+                  <tr><td colspan="7">Current adjustments</td></tr>
+                  [% total_adj = 0 %]
+                  [% FOREACH adjustment IN adjustments %]
+                  [% total_adj = total_adj + adjustment.adjustment %]
+                  <tr>
+                      <td><input type="hidden" name="adjustment_id" value="[% adjustment.adjustment_id %]" />[% adjustment.adjustment_id %]</td>
+                      <td><input type="text" name="adjustment" id="adjustment_[% adjustment.adjustment_id %]" value="[% adjustment.adjustment | $Price %]" /></td>
+                      <td>
+                          [% reasons = AuthorisedValues.Get("ADJ_REASON") %]
+                          [% IF reasons %]
+                          <select id="reason_[% adjustment.adjustment_id %]" name="reason">
+                                  <option value="">No reason</option>
+                              [% FOREACH reason IN reasons %]
+                                [% IF ( adjustment.reason == reason.authorised_value ) %]
+                                  <option selected="selected" value="[% reason.authorised_value %]">
+                                [% ELSE %]
+                                  <option value="[% reason.authorised_value %]">
+                                [% END %]
+                                  [% reason.lib %]
+                                  </option>
+                              [% END %]
+                          </select>
+                          [% ELSE %]
+                          <p title="Define values in authorised value category ADJ_REASON to enable">None</p>
+                          <input type="hidden" name="reason" id="reason_[% adjustment.adjustment_id %]" value="" />
+                          [% END %]
+                      </td>
+                      <td><input type="text" name="note" id="note_new" value="[% adjustment.note %]"/></td>
+                      <td>
+                          <select id="budget_id_[% adjustment.adjustment_id %]" name="budget_id">
+                                  <option value="">No fund</option>
+                              [% FOREACH budget IN budgets_loop %]
+                                [% IF ( budget.budget_id == adjustment.budget_id ) %]
+                                  <option selected="selected" value="[% budget.budget_id %]">
+                                [% ELSE %]
+                                  <option value="[% budget.budget_id %]">
+                                [% END %]
+                                  [% budget.budget_name %]
+                                  </option>
+                              [% END %]
+                          </select>
+                      </td>
+                      [% IF adjustment.encumber_open %]
+                      <td>
+                          <input type="checkbox" name="encumber_open" id="encumber_[% adjustment.adjustment_id %]"  value="[% adjustment.adjustment_id %]" checked/>
+                      </td>
+                      [% ELSE %]
+                      <td>
+                          <input type="checkbox" name="encumber_open" id="encumber_[% adjustment.adjustment_id %]"  value="[% adjustment.adjustment_id %]" />
+                      </td>
+                      [% END %]
+                      <td>
+                          <a class="btn btn-default btn-xs" href="/cgi-bin/koha/acqui/invoice.pl?op=del_adj&adjustment_id=[% adjustment.adjustment_id %]&invoiceid=[% invoiceid %]"><i class="fa fa-trash"></i> Delete</a>
+                      </td>
+                  </tr>
+                  [% END %]
+                  [% END %]
+                  <tr><td colspan="7">Add an adjustment</td></tr>
+                      <td><input type="hidden" name="adjustment_id" value="new" />New</td>
+                      <td><input type="text" name="adjustment" id="adjustment_new]" /></td>
+                      <td>
+                          [% reasons = AuthorisedValues.Get("ADJ_REASON") %]
+                          [% IF reasons %]
+                          <select id="reason_[% adjustment.adjustment_id %]" name="reason">
+                                  <option value="">No reason</option>
+                                [% FOREACH reason IN reasons %]
+                                  <option value="[% reason.authorised_value %]">
+                                  [% reason.lib %]
+                                  </option>
+                                [% END %]
+                          </select>
+                          [% ELSE %]
+                          <p title="Define values in authorised value category ADJ_REASON to enable">None</p>
+                          [% END %]
+                      </td>
+                      <td><input type="text" name="note" id="note_new" value=""/></td>
+                      <td>
+                          <select id="budget_id_[% adjustment.adjustment_id %]" name="budget_id">
+                               <option selected="selected" value="">No fund</option>
+                               [% FOREACH budget IN budgets_loop %]
+                               <option value="[% budget.budget_id %]">
+                               [% budget.budget_name %]
+                               </option>
+                               [% END %]
+                          </select>
+                      </td>
+                      <td><input type="checkbox" name="encumber_open" id="encumber_new" value="1" /></td>
+                      <td><input type="hidden" name="delete" value=""></td>
+                  </tr>
+                </table>
+          <input type="hidden" name="op" value="mod_adj" />
+          <input type="submit" value="Update adjustments" />
+      </form>
       <p>
           <a href="/cgi-bin/koha/acqui/parcel.pl?invoiceid=[% invoiceid %]">Go to receipt page</a>
           [% IF Koha.Preference('AcqEnableFiles') %]| <a href="/cgi-bin/koha/acqui/invoice-files.pl?invoiceid=[% invoiceid %]">Manage invoice files</a>[% END %]
                 <th>&nbsp;</th>
               </tr>
               <tr>
-                <th colspan="2">Total + Shipment cost ([% currency.symbol %])</th>
+                <th colspan="2">Total + Adjustments + Shipment cost ([% currency.symbol %])</th>
                 <th class="tax_excluded"></th>
                 <th class="tax_included"></th>
                 <th>[% total_quantity %]</th>
-                <th class="tax_excluded">[% total_tax_excluded_shipment | $Price %]</th>
+                <th class="tax_excluded">[% total_tax_excluded_shipment + total_adj | $Price %]</th>
                 <th class="tax_included">[% total_tax_included_shipment | $Price %]</th>
                 <th>&nbsp;</th>
                 <th>[% total_tax_value | $Price %]</th>
             </tfoot>
           </table>
         [% ELSE %]
-            <div class="dialog message"><p>No orders yet</p></div>
+            <div class="dialog message"><p>No orders yet</p>
+            [% IF adjustments.count > 0 || shipmentcost > 0 %]
+            <p>Adjustments plus shipping:[% total_adj + shipmentcost | $Price %]</p>
+            [% END %]
+            </div>
         [% END %]
         [% IF ( (Koha.Preference('AcqEnableFiles')) && files ) %]
             <br />
index 802ae05..174edb8 100644 (file)
 [% END %]
     </tbody>
     <tfoot>
+    [% IF ( adjustments.count > 0 ) %]
+            [% FOREACH adjustment IN adjustments %]
+                <tr>
+                    <td></td>
+                    <td colspan="6">Adjustment cost for invoice [% adjustment.invoiceid %]</td>
+                    <td class="data total">[% adjustment.adjustment %]</td>
+                </tr>
+            [% END %]
+
+    [% END %]
     <tr>
         <td> Total </td>
         <td> </td>
index ec91e86..bf41136 100644 (file)
     [% END %]
 
     <tfoot>
-        [% IF shipmentcosts.size %]
+        [% IF shipmentcosts.size || ( adjustments.count > 0 ) %]
             <tr valign="top">
                 <td colspan="9"> Sub total </td>
                 <td class="data"> [% subtotal %] </td>
             </tr>
+        [% END %]
+        [% IF shipmentcosts.size %]
             [% FOREACH shipmentcost IN shipmentcosts %]
                 <tr>
                     <td></td>
                 </tr>
             [% END %]
         [% END %]
+        [% IF ( adjustments.count > 0 ) %]
+            [% FOREACH adjustment IN adjustments %]
+                <tr>
+                    <td></td>
+                    <td colspan="8">Adjustment cost for invoice [% adjustment.invoiceid %]</td>
+                    <td class="data total">[% adjustment.adjustment %]</td>
+                </tr>
+            [% END %]
+        [% END %]
         <tr>
             <td colspan="9">TOTAL</td>
             <td class="data total">[% total %]</td>
diff --git a/t/db_dependent/Koha/Acquisition/Invoice/Adjustments.t b/t/db_dependent/Koha/Acquisition/Invoice/Adjustments.t
new file mode 100644 (file)
index 0000000..4c970e2
--- /dev/null
@@ -0,0 +1,67 @@
+#!/usr/bin/perl
+
+# Copyright 2015 Koha Development team
+#
+# 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>.
+
+use Modern::Perl;
+
+use Test::More tests => 6;
+
+use Koha::Database;
+
+use t::lib::TestBuilder;
+
+BEGIN {
+    use_ok('Koha::Acquisition::Invoice::Adjustments');
+}
+
+my $schema = Koha::Database->new->schema;
+$schema->storage->txn_begin;
+
+my $builder = t::lib::TestBuilder->new;
+my $nb_of_adjs = Koha::Acquisition::Invoice::Adjustments->search->count;
+my $budget_id = $builder->build({source=>'Aqbudget'})->{budget_id};
+my $invoice_id = $builder->build({source=>'Aqinvoice'})->{invoiceid};
+
+my $new_adj = Koha::Acquisition::Invoice::Adjustment->new({
+    note => 'noted',
+    invoiceid => $invoice_id,
+    adjustment => '3',
+    reason => 'unreasonable',
+    budget_id => $budget_id,
+})->store;
+
+like( $new_adj->adjustment_id, qr|^\d+$|, 'Adding a new adjustment should have set the adjustment_id');
+
+my $new_adj2 = Koha::Acquisition::Invoice::Adjustment->new({
+    note => 'not noted',
+    invoiceid => $invoice_id,
+    adjustment => '-3',
+    reason => 'unreasonable',
+    budget_id => $budget_id,
+})->store;
+
+ok( $new_adj->adjustment_id < $new_adj2->adjustment_id, 'Adding a new adjustment should increment');
+is( Koha::Acquisition::Invoice::Adjustments->search->count, $nb_of_adjs + 2, 'The 2 adjustments should have been added' );
+
+my $retrieved_adj = Koha::Acquisition::Invoice::Adjustments->find( $new_adj->adjustment_id );
+is( $retrieved_adj->reason, $new_adj->reason, 'Find an adjustment by id should return the correct adjustment' );
+
+$retrieved_adj->delete;
+is( Koha::Acquisition::Invoice::Adjustments->search->count, $nb_of_adjs + 1, 'Delete should have deleted the adjustment' );
+
+$schema->storage->txn_rollback;