This allows creation of special baskets that include standing orders.
These orders do not have a known quantity (and may not have a known
price in advance). Upon receipt, the received items are split into a new
completed order.
Test plan:
1) Run updatedatabase.pl.
2) Run prove t/db_dependent/Acquisition/StandingOrders.t . (and the
other Acquisition tests).
3) Create a new basket, mark it as a standing order basket.
4) Add an order to this basket, and notice that the quantity field is
missing (and thus not required).
5) Receive items for this order, and notice that the original order is
unchanged. The new child order line should have the correct price
and quantity information.
(Note: the QA tools output what seems to be a spurious spelling error
for Test::More's "isnt" in StandingOrders.t.)
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
=head3 NewBasket
- $basket = &NewBasket( $booksellerid, $authorizedby, $basketname,
- $basketnote, $basketbooksellernote, $basketcontractnumber, $deliveryplace, $billingplace );
+ $basket = &NewBasket( $booksellerid, $authorizedby, $basketname,
+ $basketnote, $basketbooksellernote, $basketcontractnumber, $deliveryplace, $billingplace, $is_standing );
Create a new basket in aqbasket table
sub NewBasket {
my ( $booksellerid, $authorisedby, $basketname, $basketnote,
$basketbooksellernote, $basketcontractnumber, $deliveryplace,
- $billingplace ) = @_;
+ $billingplace, $is_standing ) = @_;
my $dbh = C4::Context->dbh;
my $query =
'INSERT INTO aqbasket (creationdate,booksellerid,authorisedby) '
$basketnote ||= q{};
$basketbooksellernote ||= q{};
ModBasketHeader( $basket, $basketname, $basketnote, $basketbooksellernote,
- $basketcontractnumber, $booksellerid, $deliveryplace, $billingplace );
+ $basketcontractnumber, $booksellerid, $deliveryplace, $billingplace, $is_standing );
return $basket;
}
=item C<$billingplace> is the "billingplace" field in the aqbasket table.
+=item C<$is_standing> is the "is_standing" field in the aqbasket table.
+
=back
=cut
sub ModBasketHeader {
- my ($basketno, $basketname, $note, $booksellernote, $contractnumber, $booksellerid, $deliveryplace, $billingplace) = @_;
+ my ($basketno, $basketname, $note, $booksellernote, $contractnumber, $booksellerid, $deliveryplace, $billingplace, $is_standing) = @_;
my $query = qq{
UPDATE aqbasket
- SET basketname=?, note=?, booksellernote=?, booksellerid=?, deliveryplace=?, billingplace=?
+ SET basketname=?, note=?, booksellernote=?, booksellerid=?, deliveryplace=?, billingplace=?, is_standing=?
WHERE basketno=?
};
my $dbh = C4::Context->dbh;
my $sth = $dbh->prepare($query);
- $sth->execute($basketname, $note, $booksellernote, $booksellerid, $deliveryplace, $billingplace, $basketno);
+ $sth->execute($basketname, $note, $booksellernote, $booksellerid, $deliveryplace, $billingplace, $is_standing, $basketno);
if ( $contractnumber ) {
my $query2 ="UPDATE aqbasket SET contractnumber=? WHERE basketno=?";
}
my $result_set = $dbh->selectall_arrayref(
-q{SELECT * FROM aqorders WHERE biblionumber=? AND aqorders.ordernumber=?},
+q{SELECT *, aqbasket.is_standing FROM aqorders LEFT JOIN aqbasket USING (basketno) WHERE biblionumber=? AND aqorders.ordernumber=?},
{ Slice => {} }, $biblionumber, $ordernumber
);
my $order = $result_set->[0];
my $new_ordernumber = $ordernumber;
- if ( $order->{quantity} > $quantrec ) {
+ if ( $order->{is_standing} || $order->{quantity} > $quantrec ) {
# Split order line in two parts: the first is the original order line
# without received items (the quantity is decreased),
# the second part is a new order line with quantity=quantityrec
my $sth = $dbh->prepare($query);
$sth->execute(
- $order->{quantity} - $quantrec,
+ ( $order->{quantity} < $quantrec ? 0 : ( $order->{quantity} - $quantrec ) ),
( defined $order_internalnote ? $order_internalnote : () ),
( defined $order_vendornote ? $order_vendornote : () ),
$ordernumber
};
if ( $pending or $ordered ) {
- $query .= q{ AND (quantity > quantityreceived OR quantityreceived is NULL)};
- }
- if ( $ordered ) {
- $query .= q{ AND aqorders.orderstatus IN ( "ordered", "partial" )};
+ $query .= q{
+ AND (
+ ( aqbasket.is_standing AND aqorders.orderstatus IN ( "new", "ordered", "partial" ) )
+ OR (
+ ( quantity > quantityreceived OR quantityreceived is NULL )
+ };
+
+ if ( $ordered ) {
+ $query .= q{ AND aqorders.orderstatus IN ( "ordered", "partial" )};
+ }
+ $query .= q{
+ )
+ )
+ };
}
my $userenv = C4::Context->userenv;
my ($self) = @_;
# if these parameters are missing, we can't continue
- for my $key (qw( basketno quantity biblionumber budget_id )) {
+ for my $key (qw( basketno biblionumber budget_id )) {
croak "Cannot insert order: Mandatory parameter $key is missing"
unless $self->{$key};
}
+ my $schema = Koha::Database->new->schema;
+ if ( !$self->{quantity} && !$schema->resultset('Aqbasket')->find( $self->{basketno} )->is_standing ) {
+ croak "Cannot insert order: Quantity is mandatory for non-standing orders";
+ }
+
$self->{quantityreceived} ||= 0;
$self->{entrydate} ||=
output_pref( { dt => dt_from_string, dateformat => 'iso' } );
- my $schema = Koha::Database->new->schema;
my @columns = $schema->source('Aqorder')->columns;
$self->{ordernumber} ||= undef;
is_nullable: 1
size: 10
+=head2 is_standing
+
+ data_type: 'tinyint'
+ default_value: 0
+ is_nullable: 0
+
=cut
__PACKAGE__->add_columns(
{ data_type => "varchar", is_nullable => 1, size => 10 },
"branch",
{ data_type => "varchar", is_foreign_key => 1, is_nullable => 1, size => 10 },
+ "is_standing",
+ { data_type => "tinyint", default_value => 0, is_nullable => 0 },
);
=head1 PRIMARY KEY
__PACKAGE__->many_to_many("borrowernumbers", "aqbasketusers", "borrowernumber");
-# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-02 11:37:47
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:tsMzwP7eofOR27sfZSTqFQ
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2016-01-14 16:28:06
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CuP/8SFJOD4h4XZf7AdeiA
# You can replace this text with custom content, and it will be preserved on regeneration
$orderinfo->{'uncertainprice'} ||= 0;
$orderinfo->{subscriptionid} ||= undef;
+my $basketno=$$orderinfo{basketno};
+my $basket = GetBasket($basketno);
+
my $user = $input->remote_user;
# create, modify or delete biblio
# create if $quantity>0 and $existing='no'
# modify if $quantity>0 and $existing='yes'
-if ( $orderinfo->{quantity} ne '0' ) {
+if ( $basket->{is_standing} || $orderinfo->{quantity} ne '0' ) {
#TODO:check to see if biblio exists
unless ( $$orderinfo{biblionumber} ) {
#if it doesn't create it
}
-my $basketno=$$orderinfo{basketno};
my $booksellerid=$$orderinfo{booksellerid};
if (my $import_batch_id=$$orderinfo{import_batch_id}) {
print $input->redirect("/cgi-bin/koha/acqui/addorderiso2709.pl?import_batch_id=$import_batch_id&basketno=$basketno&booksellerid=$booksellerid");
$template->param(
basketno => $basketno,
+ basket => $basket,
basketname => $basket->{'basketname'},
basketbranchname => C4::Branch::GetBranchName($basket->{branch}),
basketnote => $basket->{note},
users => \@basketusers,
closedate => $basket->{closedate},
estimateddeliverydate=> $estimateddeliverydate,
+ is_standing => $basket->{is_standing},
deliveryplace => C4::Branch::GetBranchName( $basket->{deliveryplace} ),
billingplace => C4::Branch::GetBranchName( $basket->{billingplace} ),
active => $bookseller->{'active'},
basketgroups => $basketgroups,
basketgroup => $basketgroup,
grouped => $basket->{basketgroupid},
- unclosable => @orders ? 0 : 1,
+ # The double negatives and booleans here mean:
+ # "A basket cannot be closed if there are no orders in it or it's a standing order basket."
+ #
+ # (The template has another implicit restriction that the order cannot be closed if there
+ # are any orders with uncertain prices.)
+ unclosable => @orders ? $basket->{is_standing} : 1,
has_budgets => $has_budgets,
duplinbatch => $duplinbatch,
);
if ( !defined $order->{quantityreceived} ) {
$order->{quantityreceived} = 0;
}
- my $budget = GetBudget( $order->{'budget_id'} );
+ my $budget = GetBudget($order->{budget_id});
+ my $basket = GetBasket($order->{basketno});
my %line = %{ $order };
- $line{order_received} = ( $qty == $order->{'quantityreceived'} );
+ # Don't show unreceived standing orders as received
+ $line{order_received} = ( $qty == $order->{'quantityreceived'} && ( $basket->{is_standing} ? $qty : 1 ) );
$line{basketno} = $basketno;
$line{budget_name} = $budget->{budget_name};
foreach my $key (qw(transferred_from transferred_to)) {
if ($line{$key}) {
my $order = GetOrder($line{$key});
- my $basket = GetBasket($order->{basketno});
my $bookseller = Koha::Acquisition::Bookseller->fetch({ id => $basket->{booksellerid} });
$line{$key} = {
order => $order,
$input->param('basketbooksellerid'),
$input->param('deliveryplace'),
$input->param('billingplace'),
+ $input->param('is_standing') ? 1 : undef,
);
} else { #New basket
$basketno = NewBasket(
$input->param('basketcontractnumber') || undef,
$input->param('deliveryplace'),
$input->param('billingplace'),
+ $input->param('is_standing') ? 1 : undef,
);
}
print $input->redirect('basket.pl?basketno='.$basketno);
ordernumber => $ordernumber,
# basket informations
basketno => $basketno,
+ basket => $basket,
basketname => $basket->{'basketname'},
basketnote => $basket->{'note'},
booksellerid => $basket->{'booksellerid'},
--- /dev/null
+ALTER TABLE aqbasket ADD COLUMN is_standing TINYINT(1) NOT NULL DEFAULT 0;
`deliveryplace` varchar(10) default NULL, -- basket delivery place
`billingplace` varchar(10) default NULL, -- basket billing place
branch varchar(10) default NULL, -- basket branch
+ is_standing TINYINT(1) NOT NULL DEFAULT 0, -- orders in this basket are standing
PRIMARY KEY (`basketno`),
KEY `booksellerid` (`booksellerid`),
KEY `basketgroupid` (`basketgroupid`),
[% IF ( creationdate ) %]<li><span class="label">Opened on:</span> [% creationdate | $KohaDates %]</li>[% END %]
[% IF ( closedate ) %]<li><span class="label">Closed on:</span> [% closedate | $KohaDates %]</li>[% END %]
[% IF ( estimateddeliverydate ) %]<li><span class="label">Estimated delivery date:</span> [% estimateddeliverydate | $KohaDates %]</li>[% END %]
+ [% IF ( estimateddeliverydate ) %]<li><span class="label">Estimated delivery date:</span> [% estimateddeliverydate | $KohaDates %]</li>[% END %]
+ <li><span class="label">Is standing order basket:</span> [% IF is_standing %]Yes[% ELSE %]No[% END %]</li>
+
</ol>
</div>
<td class="number gste [% IF books_loo.ecostgste.search(zero_regex) %]error[% END %]">[% books_loo.ecostgste | $Price%]</td>
<td class="number gsti [% IF books_loo.rrpgsti.search(zero_regex) %]error[% END %]">[% books_loo.rrpgsti | $Price %]</td>
<td class="number gsti [% IF books_loo.ecostgsti.search(zero_regex) %]error[% END %]">[% books_loo.ecostgsti | $Price %]</td>
- <td class="number [% IF books_loo.quantity.search(zero_regex) %]error[% END %]">[% books_loo.quantity %]</td>
+ <td class="number [% IF books_loo.quantity.search(zero_regex) %]error[% END %]">[% IF is_standing and books_loo.quantity == 0 %]N/A[% ELSE %][% books_loo.quantity or 'N/A' %][% END %]</td>
<td class="number gste [% IF books_loo.totalgste.search(zero_regex) %]error[% END %]">[% books_loo.totalgste | $Price %]</td>
<td class="number gsti [% IF books_loo.totalgsti.search(zero_regex) %]error[% END %]">[% books_loo.totalgsti | $Price %]</td>
<td class="number">[% books_loo.gstrate * 100 | $Price %]</td>
</select>
</li>
[% END %]
+ <li>
+ <label for="is_standing">Orders are standing:</label>
+ <input type="checkbox" id="is_standing" name="is_standing"/>
+ <div class="hint">Standing orders do not close when received.</div>
+ </li>
</ol>
</fieldset>
<fieldset class="action">
_alertString += "\n- "+ _("You must select a fund");
}
+[% UNLESS basket.is_standing %]
if (!(isNum(ff.quantity,0)) || ff.quantity.value == 0){
ok=1;
_alertString += "\n- " + _("Quantity must be greater than '0'");
}
+[% END %]
if (!(isNum(ff.listprice,0))){
ok=1;
<fieldset class="rows">
<legend>Accounting details</legend>
<ol>
- <li>
- [% IF ( close ) %]
- <span class="label required">Quantity: </span>
- <input type="hidden" name="quantity" value="[% quantity %]" />[% quantity %]
- [% ELSE %]
- <label class="required" for="quantity">Quantity: </label>
- [% IF (AcqCreateItemOrdering) %]
- [% IF subscriptionid %]
- <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="1" />
- [% ELSE %]
- <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="0" />
- [% END %]
+ [% UNLESS basket.is_standing %]
+ <li>
+ [% IF ( close ) %]
+ <span class="label required">Quantity: </span>
+ <input type="hidden" name="quantity" value="[% quantity %]" />[% quantity %]
[% ELSE %]
- [% IF subscriptionid %]
- <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="1" />
+ <label class="required" for="quantity">Quantity: </label>
+ [% IF (AcqCreateItemOrdering) %]
+ [% IF subscriptionid %]
+ <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="1" />
+ [% ELSE %]
+ <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="0" />
+ [% END %]
[% ELSE %]
- <input type="text" size="20" id="quantity" name="quantity" value="[% quantityrec %]" onchange="updateCosts();" />
+ [% IF subscriptionid %]
+ <input type="text" readonly="readonly" size="20" id="quantity" name="quantity" value="1" />
+ [% ELSE %]
+ <input type="text" size="20" id="quantity" name="quantity" value="[% quantityrec %]" onchange="updateCosts();" />
+ [% END %]
[% END %]
+ <span class="required">Required</span>
[% END %]
- <span class="required">Required</span>
- [% END %]
- <!-- origquantityrec only here for javascript compatibility (additem.js needs it, useless here, usefull when receiveing an order -->
- <input id="origquantityrec" readonly="readonly" type="hidden" name="origquantityrec" value="1" />
- </li>
+ <!-- origquantityrec only here for javascript compatibility (additem.js needs it, useless here, usefull when receiveing an order -->
+ <input id="origquantityrec" readonly="readonly" type="hidden" name="origquantityrec" value="1" />
+ </li>
+ [% END %]
<li>
[% IF ( close ) %]
<span class="label required">Fund: </span>
};
$return_error = $@;
my $expected_error = "Cannot insert order: Mandatory parameter $mandatoryparams_key is missing";
+ if ( $mandatoryparams_key eq 'quantity' ) {
+ $expected_error = "Cannot insert order: Quantity is mandatory for non-standing orders";
+ }
ok(
( !( defined $order ) )
&& ( index( $return_error, $expected_error ) >= 0 ),
is( $order->{quantityreceived}, 0, 'Koha::Acquisition::Order->insert set quantityreceivedto 0 if undef is given' );
is( $order->{entrydate}, output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }), 'Koha::Acquisition::Order->insert set entrydate to today' );
-$schema->storage->txn_rollback();
\ No newline at end of file
+$schema->storage->txn_rollback();
--- /dev/null
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+use Test::More tests => 14;
+use C4::Context;
+use C4::Acquisition;
+use C4::Biblio;
+use C4::Items;
+use C4::Bookseller;
+use C4::Budgets;
+use Koha::Acquisition::Order;
+use t::lib::Mocks;
+use t::lib::TestBuilder;
+
+my $schema = Koha::Database->schema;
+$schema->storage->txn_begin;
+my $builder = t::lib::TestBuilder->new;
+
+# Set up configuration data
+
+my $branch = $builder->build( { source => 'Branch' } );
+my $bookseller = $builder->build( { source => 'Aqbookseller' } );
+my $budget = $builder->build( { source => 'Aqbudget' } );
+my $staffmember = $builder->build( { source => 'Borrower' } );
+
+# Create baskets and orders
+
+my $basketno = NewBasket(
+ $bookseller->{id},
+ $staffmember->{borrowernumber},
+ 'Standing order basket', # basketname
+ '', # basketnote
+ '', # basketbooksellernote
+ undef, # basketcontractnumber
+ $branch->{branchcode}, # deliveryplace
+ $branch->{branchcode}, # billingplace
+ 1 # is_standing
+);
+
+my $nonstandingbasketno = NewBasket(
+ $bookseller->{id},
+ $staffmember->{borrowernumber},
+ 'Non-standing order basket', # basketname
+ '', # basketnote
+ '', # basketbooksellernote
+ undef, # basketcontractnumber
+ $branch->{branchcode}, # deliveryplace
+ $branch->{branchcode}, # billingplace
+ 0 # is_standing
+);
+
+my $basket = GetBasket($basketno);
+
+is( $basket->{is_standing}, 1, 'basket correctly created as standing order basket' );
+
+my ( $biblionumber, $biblioitemnumber ) = C4::Biblio::AddBiblio( MARC::Record->new, '' );
+
+my $ordernumber = Koha::Acquisition::Order->new(
+ {
+ basketno => $basketno,
+ biblionumber => $biblionumber,
+ budget_id => $budget->{budget_id},
+ currency => 'USD',
+ quantity => 0,
+ }
+)->insert->{ordernumber};
+
+isnt( $ordernumber, undef, 'standing order successfully created' );
+
+eval {
+ Koha::Acquisition::Order->new(
+ {
+ basketno => $nonstandingbasketno,
+ biblionumber => $biblionumber,
+ budget_id => $budget->{budget_id},
+ currency => 'USD',
+ quantity => 0,
+ }
+ )->insert;
+};
+
+like( $@, qr/quantity/im, 'normal orders cannot be created without quantity' );
+
+my $search_orders = SearchOrders( {
+ basketno => $basketno,
+ pending => 1,
+ ordered => 1,
+} );
+
+ok(
+ scalar @$search_orders == 1 && $search_orders->[0]->{ordernumber} == $ordernumber,
+ 'standing order counts as a pending/ordered order'
+);
+
+my $invoiceid = AddInvoice(
+ invoicenumber => 'invoice',
+ booksellerid => $bookseller->{id},
+ unknown => "unknown"
+);
+
+my ( $datereceived, $new_ordernumber ) = ModReceiveOrder(
+ {
+ biblionumber => $biblionumber,
+ ordernumber => $ordernumber,
+ quantityreceived => 2,
+ cost => 12,
+ ecost => 22,
+ invoiceid => $invoiceid,
+ rrp => 42,
+ }
+);
+
+isnt( $ordernumber, $new_ordernumber, "standing order split on receive" );
+
+my $order = Koha::Acquisition::Order->fetch( { ordernumber => $ordernumber } );
+my $neworder = Koha::Acquisition::Order->fetch( { ordernumber => $new_ordernumber } );
+
+is( $order->{orderstatus}, 'partial', 'original order set to partially received' );
+is( $order->{quantity}, 0, 'original order quantity unchanged' );
+is( $order->{quantityreceived}, 0, 'original order has no received items' );
+isnt( $order->{unitprice}, 12, 'original order does not get cost' );
+is( $neworder->{orderstatus}, 'complete', 'new order set to complete' );
+is( $neworder->{quantityreceived}, 2, 'new order has received items' );
+cmp_ok( $neworder->{unitprice}, '==', 12, 'new order does get cost' );
+
+$search_orders = SearchOrders( {
+ basketno => $basketno,
+ pending => 1,
+ ordered => 1,
+} );
+
+is( scalar @$search_orders, 1, 'only one pending order after receive' );
+is( $search_orders->[0]->{ordernumber}, $ordernumber, 'original order is only pending order' );
+
+$schema->storage->txn_rollback();