View | Details | Raw Unified | Return to bug 23354
Collapse All | Expand All

(-)a/Koha/Charges/Sales.pm (+251 lines)
Line 0 Link Here
1
package Koha::Charges::Sales;
2
3
# Copyright 2019 PTFS Europe
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Koha::Account::Lines;
23
use Koha::Account::Offsets;
24
use Koha::DateUtils qw( dt_from_string );
25
use Koha::Exceptions;
26
27
=head1 NAME
28
29
Koha::Charges::Sale - Module for collecting sales in Koha
30
31
=head1 SYNOPSIS
32
33
  use Koha::Charges::Sale;
34
35
  my $sale = Koha::Charges::Sale->new( cash_register => $register );
36
  $sale->add_item($item);
37
  $sale->purchase;
38
39
=head2 Class methods
40
41
=head3 new
42
43
  Koha::Charges::Sale->new(
44
    {
45
        cash_register  => $cash_register,
46
        user_id        => $user_id,
47
        [ payment_type => $payment_type ],
48
        [ items        => $items ],
49
        [ patron       => $patron ],
50
    }
51
  );
52
53
=cut
54
55
sub new {
56
    my ( $class, $params ) = @_;
57
58
    Koha::Exceptions::MissingParameter->throw(
59
        "Missing mandatory parameter: cash_register")
60
      unless $params->{cash_register};
61
    Carp::confess("Key 'cash_register' is not a Koha::Cash::Register object!")
62
      unless $params->{cash_register}->isa('Koha::Cash::Register');
63
64
    $params->{valid_items} = {
65
        map { $_ => 1 } Koha::AuthorisedValues->search(
66
            {
67
                category   => 'MANUAL_INV',
68
                branchcode => $params->{cash_register}->branch
69
            }
70
        )->get_column('authorised_value')
71
    };
72
73
    return bless( $params, $class );
74
}
75
76
=head3 cash_register
77
78
  my $cash_register = $sales->cash_register( $cash_register );
79
80
=cut
81
82
sub cash_register {
83
    my ( $self, $cash_register ) = @_;
84
85
    $self->{cash_register} = $cash_register
86
      if $cash_register && $cash_register->isa('Koha::Cash::Register');
87
88
    return $self->{cash_register};
89
}
90
91
=head3 payment_type
92
93
  my $payment_type = $sale->payment_type( $payment_type );
94
95
=cut
96
97
sub payment_type {
98
    my ( $self, $payment_type ) = @_;
99
100
    $self->{payment_type} = $payment_type;
101
102
    return $self;
103
}
104
105
=head3 patron
106
107
  my $patron = $sale->patron( $patron );
108
109
=cut
110
111
sub patron {
112
    my ( $self, $patron ) = @_;
113
114
    $self->{patron} = $patron if $patron && $patron->isa('Koha::Patron');
115
116
    return $self->{patron};
117
}
118
119
=head3 items
120
121
  my $items = $sale->items;
122
123
=cut
124
125
sub items {
126
    my ( $self, $item ) = @_;
127
128
    $self->{item} = $item if $item && $item->isa('Koha::Item');
129
130
    return $self->{item};
131
}
132
133
=head3 add_item
134
135
  my $item = { price => 0.25, quantity => 1, code => 'COPY' };
136
  $sale->add_item( $item );
137
138
=cut
139
140
sub add_item {
141
    my ( $self, $item ) = @_;
142
143
    Koha::Exceptions::MissingParameter->throw(
144
        "Missing mandatory parameter: code")
145
      unless $item->{code};
146
147
    Koha::Exceptions::Account::UnrecognisedType->throw(
148
        error => 'Type of debit not recognised' )
149
      unless ( exists( $self->{valid_items}->{ $item->{code} } ) );
150
151
    Koha::Exceptions::MissingParameter->throw(
152
        "Missing mandatory parameter: price")
153
      unless $item->{price};
154
155
    Koha::Exceptions::MissingParameter->throw(
156
        "Missing mandatory parameter: quantity")
157
      unless $item->{quantity};
158
159
    push @{ $self->{items} }, $item;
160
    return $self;
161
}
162
163
=head3 purchase
164
165
  my $credit_line = $sale->purchase;
166
167
=cut
168
169
sub purchase {
170
    my ( $self, $params ) = @_;
171
172
    my $payment_type =
173
      exists( $params->{payment_type} )
174
      ? $params->{payment_type}
175
      : $self->{payment_type};
176
177
    Koha::Exceptions::MissingParameter->throw(
178
        "Missing mandatory parameter: payment_type")
179
      unless $payment_type;
180
181
    my $schema     = Koha::Database->new->schema;
182
    my $dt = dt_from_string();
183
    my $total_owed = 0;
184
    my $credit;
185
186
    $schema->txn_do(
187
        sub {
188
189
            my $debit_offsets;
190
            for my $item ( @{ $self->{items} } ) {
191
192
                my $amount = $item->{quantity} * $item->{price};
193
                $total_owed = $total_owed + $amount;
194
195
                # Insert the account line
196
                my $line = Koha::Account::Line->new(
197
                    {
198
                        amount            => $amount,
199
                        accounttype       => $item->{code},
200
                        amountoutstanding => 0,
201
                        note              => $item->{quantity},
202
                        manager_id        => $self->{user_id},
203
                        interface         => 'intranet',
204
                        branchcode        => $self->{cash_register}->branch,
205
                        date              => $dt
206
                    }
207
                )->store();
208
209
                # Record the account offset
210
                my $account_offset = Koha::Account::Offset->new(
211
                    {
212
                        debit_id => $line->id,
213
                        type     => 'Payment',
214
                        amount   => $amount * -1
215
                    }
216
                )->store();
217
218
                push @{$debit_offsets}, $account_offset;
219
            }
220
221
            $credit = Koha::Account::Line->new(
222
                {
223
                    amount            => 0 - $total_owed,
224
                    accounttype       => 'Pay',
225
                    payment_type      => $payment_type,
226
                    amountoutstanding => 0,
227
                    manager_id        => $self->{user_id},
228
                    interface         => 'intranet',
229
                    branchcode        => $self->{cash_register}->branch,
230
                    register_id       => $self->{cash_register}->id,
231
                    date              => $dt,
232
                    note              => "POS SALE"
233
                }
234
            )->store();
235
236
            for my $offset (@{$debit_offsets}) {
237
                $offset->credit_id( $credit->accountlines_id )->store();
238
            }
239
        }
240
    );
241
242
    return $credit;
243
}
244
245
=head1 AUTHOR
246
247
Martin Renvoize <martin.renvoize@ptfs-europe.com>
248
249
=cut
250
251
1;
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/pos-menu.inc (+18 lines)
Line 0 Link Here
1
<div id="navmenu">
2
    <div id="navmenulist">
3
        <h5>Point of sale</h5>
4
        <ul>
5
            <li>Cashup register</li>
6
        </ul>
7
        <h5>Administration</h5>
8
        <ul>
9
            [% IF ( CAN_user_cash_management_manage_cash_registers ) %]
10
                <li><a href="/cgi-bin/koha/admin/cash_registers.pl">Cash registers</a></li>
11
            [% END %]
12
13
            [% IF ( CAN_user_parameters_manage_auth_values ) %]
14
                <li><a href="/cgi-bin/koha/admin/authorised_values.pl?searchfield=MANUAL_INV">Purchase items</a></li>
15
            [% END %]
16
        </ul>
17
    </div>
18
</div>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt (+4 lines)
Lines 80-85 Link Here
80
                    <div class="col-xs-6">
80
                    <div class="col-xs-6">
81
                        <ul class="biglinks-list">
81
                        <ul class="biglinks-list">
82
82
83
                            <li>
84
                                <a class="icon_general icon_pos" href="/cgi-bin/koha/pos/pay.pl">POS</a>
85
                            </li>
86
83
                            [% IF ( CAN_user_editcatalogue_edit_catalogue || CAN_user_editcatalogue_edit_items ) %]
87
                            [% IF ( CAN_user_editcatalogue_edit_catalogue || CAN_user_editcatalogue_edit_items ) %]
84
                            <li>
88
                            <li>
85
                                <a class="icon_general icon_cataloging" href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a>
89
                                <a class="icon_general icon_cataloging" href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt (+319 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE Asset %]
3
[% USE Koha %]
4
[% USE AuthorisedValues %]
5
[% USE Price %]
6
[% SET footerjs = 1 %]
7
[% INCLUDE 'doc-head-open.inc' %]
8
<title>Koha &rsaquo; Payments</title>
9
[% INCLUDE 'doc-head-close.inc' %]
10
</head>
11
12
<body id="payments" class="pos">
13
[% INCLUDE 'header.inc' %]
14
[% INCLUDE 'circ-search.inc' %]
15
16
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; Point of sale</div>
17
18
<div class="main container-fluid">
19
    <div class="row">
20
        <div class="col-sm-10 col-sm-push-2">
21
22
        [% IF ( error_registers ) %]
23
        <div id="error_message" class="dialog alert">
24
            You must have at least one cash register associated with this branch before you can record payments.
25
        </div>
26
        [% ELSE %]
27
        <form name="payForm" id="payForm" method="post" action="/cgi-bin/koha/pos/pay.pl">
28
            <div class="row">
29
30
                <div class="col-sm-6">
31
32
                    <fieldset class="rows">
33
                        <legend>This sale</legend>
34
                        <p>Click to edit item cost or quantities</p>
35
                        <table id="sale" class="table_sale">
36
                            <thead>
37
                                <tr>
38
                                    <th>Item</th>
39
                                    <th>Cost</th>
40
                                    <th>Quantity</th>
41
                                    <th>Total</th>
42
                                </tr>
43
                            </thead>
44
                            <tbody>
45
                            </tbody>
46
                            <tfoot>
47
                                <tr>
48
                                    <td colspan="3">Total payable:</td>
49
                                    <td></td>
50
                                </tr>
51
                            </tfoot>
52
                        </table>
53
                    </fieldset>
54
55
                    <fieldset class="rows">
56
                        <legend>Collect payment</legend>
57
                        <ol>
58
                            <li>
59
                                <label for="paid">Amount being paid: </label>
60
                                <input name="paid" id="paid" value="[% amountoutstanding | $Price on_editing => 1 %]"/>
61
                            </li>
62
                            <li>
63
                                <label for="collected">Collected from patron: </label>
64
                                <input id="collected" value="[% amountoutstanding | $Price on_editing => 1 %]"/>
65
                            </li>
66
                            <li>
67
                                <label>Change to give: </label>
68
                                <span id="change">0.00</span>
69
                            </li>
70
71
                            [% SET payment_types = AuthorisedValues.GetAuthValueDropbox('PAYMENT_TYPE') %]
72
                            [% IF payment_types %]
73
                            <li>
74
                                <label for="payment_type">Payment type: </label>
75
                                <select name="payment_type" id="payment_type">
76
                                    [% FOREACH pt IN payment_types %]
77
                                        <option value="[% pt.authorised_value | html %]">[% pt.lib | html %]</option>
78
                                    [% END %]
79
                                </select>
80
                            </li>
81
                            [% END %]
82
83
                            [% IF Koha.Preference('UseCashRegisters') %]
84
                            <li>
85
                                <label for="cash_register">Cash register: </label>
86
                                <select name="cash_register" id="cash_register">
87
                                    [% FOREACH register IN registers %]
88
                                      [% IF register.id == registerid %]
89
                                    <option value="[% register.id %]" selected="selected">[% register.name | html %]</option>
90
                                      [% ELSE %]
91
                                    <option value="[% register.id %]">[% register.name | html %]</option>
92
                                      [% END %]
93
                                    [% END %]
94
                                </select>
95
                            </li>
96
                            [% END %]
97
                        </ol>
98
99
                    </fieldset>
100
                </div>
101
102
                <div class="col-sm-6">
103
                    <fieldset class="rows">
104
                        <legend>Items for purchase</legend>
105
                            [% SET invoice_types = AuthorisedValues.GetAuthValueDropbox('MANUAL_INV') %]
106
                            [% IF invoice_types %]
107
                            <table id="invoices">
108
                            <thead>
109
                                <tr>
110
                                    <th>Code</th>
111
                                    <th>Description</th>
112
                                    <th>Cost</th>
113
                                    <th>Action</th>
114
                                </tr>
115
                            </thead>
116
                            <tbody>
117
                            [% FOREACH invoice IN invoice_types %]
118
                                <tr>
119
                                    <td>[% invoice.authorised_value | html %]</td>
120
                                    <td>[% invoice.lib_opac | html %]</td>
121
                                    <td>[% invoice.lib | html %]</td>
122
                                    <td>
123
                                        <button class="add_button" data-invoice-code="[% invoice.lib_opac %]" data-invoice-title="[% invoice.authorised_value | html %]" data-invoice-price="[% invoice.lib | html %]"><i class="fa fa-plus"></i> Add</button>
124
                                    </td>
125
                                </tr>
126
                            [% END %]
127
                            </table>
128
                            [% ELSE %]
129
                            You have no manual invoice types defined
130
                            [% END %]
131
                    </fieldset>
132
                </div>
133
134
                <div class="action">
135
                    <input type="submit" name="submitbutton" value="Confirm" />
136
                    <a class="cancel" href="/cgi-bin/koha/pos/pay.pl">Cancel</a>
137
                </div>
138
            </div>
139
        </form>
140
        [% END %]
141
    </div>
142
143
    <div class="col-sm-2 col-sm-pull-10">
144
        <aside>
145
            [% INCLUDE 'pos-menu.inc' %]
146
        </aside>
147
    </div>
148
149
</div> <!-- /.row -->
150
151
[% MACRO jsinclude BLOCK %]
152
    [% Asset.js("js/admin-menu.js") | $raw %]
153
    [% INCLUDE 'datatables.inc' %]
154
    [% Asset.js("lib/jquery/plugins/jquery.jeditable.mini.js") | $raw %]
155
    <script>
156
    function fnClickAddRow( table, invoiceTitle, invoicePrice ) {
157
      table.fnAddData( [
158
        invoiceTitle,
159
        invoicePrice,
160
        1,
161
        null
162
         ]
163
      );
164
    }
165
166
    function moneyFormat(textObj) {
167
        var newValue = textObj.value;
168
        var decAmount = "";
169
        var dolAmount = "";
170
        var decFlag   = false;
171
        var aChar     = "";
172
173
        for(i=0; i < newValue.length; i++) {
174
            aChar = newValue.substring(i, i+1);
175
            if (aChar >= "0" && aChar <= "9") {
176
                if(decFlag) {
177
                    decAmount = "" + decAmount + aChar;
178
                }
179
                else {
180
                    dolAmount = "" + dolAmount + aChar;
181
                }
182
            }
183
            if (aChar == ".") {
184
                if (decFlag) {
185
                    dolAmount = "";
186
                    break;
187
                }
188
                decFlag = true;
189
            }
190
        }
191
192
        if (dolAmount == "") {
193
            dolAmount = "0";
194
        }
195
    // Strip leading 0s
196
        if (dolAmount.length > 1) {
197
            while(dolAmount.length > 1 && dolAmount.substring(0,1) == "0") {
198
                dolAmount = dolAmount.substring(1,dolAmount.length);
199
            }
200
        }
201
        if (decAmount.length > 2) {
202
            decAmount = decAmount.substring(0,2);
203
        }
204
    // Pad right side
205
        if (decAmount.length == 1) {
206
           decAmount = decAmount + "0";
207
        }
208
        if (decAmount.length == 0) {
209
           decAmount = decAmount + "00";
210
        }
211
212
        textObj.value = dolAmount + "." + decAmount;
213
    }
214
215
    function updateChangeValues() {
216
        var change = $('#change')[0];
217
        change.innerHTML = Math.round(($('#collected')[0].value - $('#paid')[0].value) * 100) / 100;
218
        if (change.innerHTML <= 0) {
219
            change.innerHTML = "0.00";
220
        } else {
221
            change.value = change.innerHTML;
222
            moneyFormat(change);
223
            change.innerHTML = change.value;
224
        }
225
        $('#modal_change').html(change.innerHTML);
226
    }
227
228
    $(document).ready(function() {
229
        var sale_table = $("#sale").dataTable($.extend(true, {}, dataTablesDefaults, {
230
            "bPaginate": false,
231
            "bFilter": false,
232
            "bInfo": false,
233
            "bAutoWidth": false,
234
            "aoColumnDefs": [{
235
                "aTargets": [-2],
236
                "bSortable": false,
237
                "bSearchable": false,
238
            }, {
239
                "aTargets": [-1],
240
                "mRender": function ( data, type, full ) {
241
                    var price = Number.parseFloat(data).toFixed(2);
242
                    return '£'+price;
243
                }
244
            }, {
245
                "aTargets": [-2, -3],
246
                "sClass" : "editable",
247
            }],
248
            "aaSorting": [
249
                [1, "asc"]
250
            ],
251
            "fnDrawCallback": function (oSettings) {
252
                var local = this;
253
                local.$('.editable').editable( function(value, settings) {
254
                    var aPos = local.fnGetPosition( this );
255
                    local.fnUpdate( value, aPos[0], aPos[1], true, false );
256
                    return value;
257
                },{
258
                    type    : 'text'
259
                })
260
            },
261
            "fnRowCallback": function( nRow, aData, iDisplayIndex, iDisplayIndexFull ) {
262
                var iTotal = aData[1] * aData[2];
263
                this.fnUpdate( iTotal, nRow, 3, false, false );
264
            },
265
            "fnFooterCallback": function(nFoot, aData, iStart, iEnd, aiDisplay) {
266
                var iTotalPrice = 0;
267
                for ( var i=0 ; i<aData.length ; i++ )
268
			    {
269
				    iTotalPrice += aData[i][3]*1;
270
			    }
271
272
                iTotalPrice = Number.parseFloat(iTotalPrice).toFixed(2);
273
                nFoot.getElementsByTagName('td')[1].innerHTML = iTotalPrice;
274
                $('#paid').val(iTotalPrice);
275
            }
276
        }));
277
278
        var items_table = $("#invoices").dataTable($.extend(true,{}, dataTablesDefaults, {
279
               "aoColumnDefs": [
280
                  { "aTargets": [ -1, -2 ], "bSortable": false, "bSearchable":false },
281
               ],
282
               "aaSorting": [[ 0, "asc" ]],
283
               "paginationType": "four_button",
284
        }));
285
286
        $(".add_button").on("click", function(ev) {
287
            ev.preventDefault();
288
            fnClickAddRow(sale_table, $( this ).data('invoiceTitle'), $( this ).data('invoicePrice') );
289
            items_table.fnFilter( '' );
290
        });
291
292
        $("#paid, #collected").on("change",function() {
293
            moneyFormat( this );
294
            if (change != undefined) {
295
                updateChangeValues();
296
            }
297
        });
298
299
        $("#payForm").submit(function(e){
300
            var rows = sale_table.fnGetData();
301
            rows.forEach(function (row, index) {
302
                var sale = {
303
                    code: row[0],
304
                    price: row[1],
305
                    quantity: row[2]
306
                };
307
                $('<input>').attr({
308
                    type: 'hidden',
309
                    name: 'sales',
310
                    value: JSON.stringify(sale)
311
                }).appendTo('#payForm');
312
            });
313
            return true;
314
        });
315
    });
316
    </script>
317
[% END %]
318
319
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/pos/pay.pl (-1 / +80 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use CGI;
6
use JSON qw( from_json );
7
8
use C4::Auth qw/:DEFAULT get_session/;
9
use C4::Output;
10
use C4::Context;
11
12
use Koha::AuthorisedValues;
13
use Koha::Cash::Registers;
14
use Koha::Charges::Sales;
15
use Koha::Database;
16
use Koha::Libraries;
17
18
my $q         = CGI->new();
19
my $sessionID = $q->cookie('CGISESSID');
20
my $session   = get_session($sessionID);
21
22
my ( $template, $loggedinuser, $cookie, $user_flags ) = get_template_and_user(
23
    {
24
        template_name   => 'pos/pay.tt',
25
        query           => $q,
26
        type            => 'intranet',
27
        authnotrequired => 0,
28
    }
29
);
30
my $logged_in_user = Koha::Patrons->find($loggedinuser) or die "Not logged in";
31
32
my $library_id = C4::Context->userenv->{'branch'};
33
my $registerid = $q->param('registerid');
34
my $registers = Koha::Cash::Registers->search(
35
    { branch   => $library_id, archived => 0 },
36
    { order_by => { '-asc' => 'name' } }
37
);
38
39
if ( !$registers->count ) {
40
    $template->param( error_registers => 1 );
41
}
42
else {
43
    if ( !$registerid ) {
44
        my $default_register = Koha::Cash::Registers->find(
45
            { branch => $library_id, branch_default => 1 } );
46
        $registerid = $default_register->id if $default_register;
47
    }
48
    $registerid = $registers->next->id if !$registerid;
49
50
    $template->param(
51
        registerid => $registerid,
52
        registers  => $registers,
53
    );
54
}
55
56
my $total_paid = $q->param('paid');
57
if ( $total_paid and $total_paid ne '0.00' ) {
58
    warn "total_paid: $total_paid\n";
59
    my $cash_register = Koha::Cash::Registers->find( { id => $registerid } );
60
    my $payment_type = $q->param('payment_type');
61
    my $sale = Koha::Charges::Sales->new(
62
        {
63
            cash_register => $cash_register,
64
            user_id       => $logged_in_user->id,
65
            payment_type  => $payment_type
66
        }
67
    );
68
69
    my @sales = $q->multi_param('sales');
70
    for my $item ( @sales ) {
71
        $item = from_json $item;
72
        $sale->add_item($item);
73
    }
74
75
    $sale->purchase;
76
}
77
78
output_html_with_http_headers( $q, $cookie, $template->output );
79
80
1;

Return to bug 23354