1 package SL::Dev::Inventory;
5 our @EXPORT = qw(create_warehouse_and_bins set_stock transfer_stock transfer_sales_delivery_order transfer_purchase_delivery_order transfer_delivery_order_item transfer_in transfer_out);
10 use SL::DB::TransferType;
12 use SL::DB::DeliveryOrderItemsStock;
18 sub create_warehouse_and_bins {
21 my $number_of_bins = $params{number_of_bins} || 5;
22 my $wh = SL::DB::Warehouse->new(description => $params{warehouse_description} || "Warehouse", invalid => 0);
23 for my $i ( 1 .. $number_of_bins ) {
24 $wh->add_bins( SL::DB::Bin->new(description => ( $params{bin_description} || "Bin" ) . " $i" ) );
27 return ($wh, $wh->bins->[0]);
33 die "param part is missing or not an SL::DB::Part object" unless ref($params{part}) eq 'SL::DB::Part';
34 my $part = delete $params{part};
35 die "qty is missing" unless $params{qty} or $params{abs_qty};
36 die "need a bin or default bin" unless $part->warehouse_id or $part->bin_id or $params{bin} or $params{bin_id};
38 my ($warehouse_id, $bin_id);
41 die "illegal param bin: " . Dumper($params{bin}) unless ref($params{bin}) eq 'SL::DB::Bin';
42 my $bin = delete $params{bin};
44 $warehouse_id = $bin->warehouse_id;
45 } elsif ( $params{bin_id} ) {
46 my $bin = SL::DB::Manager::Bin->find_by(id => delete $params{bin_id});
48 $warehouse_id = $bin->warehouse_id;
49 } elsif ( $part->bin_id ) {
50 $bin_id = $part->bin_id;
51 $warehouse_id = $part->warehouse_id;
53 die "can't determine bin and warehouse";
56 my $employee_id = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
57 die "Can't determine employee" unless $employee_id;
59 my $qty = delete $params{qty};
61 my $transfer_type_description;
63 if ( $params{abs_qty} ) {
64 # determine the current qty and calculate the qty diff that needs to be applied
65 # if abs_qty is set then any value that was in $params{qty} is ignored/overwritten
67 $get_stock_params{bin_id} = $bin_id if $bin_id;
68 # $get_stock_params{warehouse_id} = $warehouse_id if $warehouse_id; # redundant
69 my $current_qty = $part->get_stock(%get_stock_params);
70 $qty = $params{abs_qty} - $current_qty;
74 $transfer_type_description = delete $params{transfer_type} // 'stock';
75 $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'in' );
77 $transfer_type_description = delete $params{transfer_type} // 'shipped';
78 $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'out' );
80 die "can't determine transfer_type" unless $transfer_type;
83 if ( $params{shippingdate} ) {
84 $shippingdate = delete $params{shippingdate};
85 $shippingdate = $::locale->parse_date_to_object($shippingdate) unless ref($shippingdate) eq 'DateTime';
87 $shippingdate = DateTime->today;
91 if ( $params{unit} ) {
92 $unit = delete $params{unit};
93 $unit = SL::DB::Manager::Unit->find_by( name => $unit ) unless ref($unit) eq 'SL::DB::Unit';
94 $qty = $unit->convert_to($qty, $part->unit_obj);
97 my ($trans_id) = $part->db->dbh->selectrow_array("select nextval('id')", {});
99 SL::DB::Inventory->new(
100 parts_id => $part->id,
102 warehouse_id => $warehouse_id,
103 employee_id => $employee_id,
104 trans_type_id => $transfer_type->id,
105 comment => $params{comment},
106 shippingdate => $shippingdate,
108 trans_id => $trans_id,
116 die "missing params" unless ( $params{parts_id} or $params{part} ) and $params{from_bin} and $params{to_bin};
119 if ( $params{parts_id} ) {
120 $part = SL::DB::Manager::Part->find_by( id => delete $params{parts_id} ) or die "illegal parts_id";
122 $part = delete $params{part};
124 die "illegal part" unless ref($part) eq 'SL::DB::Part';
126 my $from_bin = delete $params{from_bin};
127 my $to_bin = delete $params{to_bin};
128 die "illegal bins" unless ref($from_bin) eq 'SL::DB::Bin' and ref($to_bin) eq 'SL::DB::Bin';
130 my $qty = delete($params{qty});
131 die "qty must be > 0" unless $qty > 0;
134 my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'transfer') or die "can't determine transfer type";
135 my $employee_id = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
138 'bestbefore' => undef,
139 'change_default_bin' => undef,
140 'chargenumber' => '',
141 'comment' => delete $params{comment} // '',
142 'dst_bin_id' => $to_bin->id,
143 'dst_warehouse_id' => $to_bin->warehouse_id,
144 'parts_id' => $part->id,
146 'src_bin_id' => $from_bin->id,
147 'src_warehouse_id' => $from_bin->warehouse_id,
148 'transfer_type_id' => $transfer_type->id,
151 WH->transfer($WH_params);
155 # do it manually via rose:
158 # my $db = SL::DB::Inventory->new->db;
159 # $db->with_transaction(sub{
160 # ($trans_id) = $db->dbh->selectrow_array("select nextval('id')", {});
161 # die "no trans_id" unless $trans_id;
164 # shippingdate => delete $params{shippingdate} // DateTime->today,
165 # employee_id => $employee_id,
166 # trans_id => $trans_id,
167 # trans_type_id => $transfer_type->id,
168 # parts_id => $part->id,
169 # comment => delete $params{comment} || 'Umlagerung',
172 # SL::DB::Inventory->new(
173 # warehouse_id => $from_bin->warehouse_id,
174 # bin_id => $from_bin->id,
179 # SL::DB::Inventory->new(
180 # warehouse_id => $to_bin->warehouse_id,
181 # bin_id => $to_bin->id,
185 # }) or die $@ . "\n";
192 my $transfer_type = delete $params{transfer_type};
194 die "param transfer_type is not a SL::DB::TransferType object: " . Dumper($transfer_type) unless ref($transfer_type) eq 'SL::DB::TransferType';
196 my $shippingdate = delete $params{shippingdate} // DateTime->today;
198 my $part = delete($params{part}) or croak 'part missing';
199 my $qty = delete($params{qty}) or croak 'qty missing';
201 # distinguish absolute qty in inventory depending on transfer type direction
202 $qty *= -1 if $transfer_type->direction eq 'out';
204 # use defaults for unit/wh/bin is they exist and nothing else is specified
205 my $unit = delete($params{unit}) // $part->unit or croak 'unit missing';
206 my $bin = delete($params{bin}) // $part->bin or croak 'bin missing';
207 # if bin is given, we don't need a warehouse param
208 my $wh = $bin->warehouse or croak 'wh missing';
211 parts_id => $part->id,
215 transfer_type => $transfer_type,
217 comment => delete $params{comment},
218 shippingdate => $shippingdate,
225 my $transfer_type = delete $params{transfer_type} // 'stock';
227 my $transfer_type_obj = SL::DB::Manager::TransferType->find_by( direction => 'in', description => $transfer_type ) or die "Can't find transfer_type with direction in and descriptin " . $params{transfer_type};
229 $params{transfer_type} = $transfer_type_obj;
237 my $transfer_type = delete $params{transfer_type} // 'shipped';
239 my $transfer_type_obj = SL::DB::Manager::TransferType->find_by( direction => 'out', description => $transfer_type ) or die "Can't find transfer_type with direction in and descriptin " . $params{transfer_type};
241 $params{transfer_type} = $transfer_type_obj;
246 sub transfer_sales_delivery_order {
247 my ($sales_delivery_order) = @_;
248 die "first argument must be a sales delivery order Rose DB object" unless ref($sales_delivery_order) eq 'SL::DB::DeliveryOrder' and $sales_delivery_order->is_sales;
250 die "the delivery order has already been delivered" if $sales_delivery_order->delivered;
252 my ($wh, $bin, $trans_type);
254 $sales_delivery_order->db->with_transaction(sub {
256 foreach my $doi ( @{ $sales_delivery_order->items } ) {
257 next if $doi->part->is_service or $doi->part->is_assortment;
258 my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
259 transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
261 $sales_delivery_order->delivered(1);
262 $sales_delivery_order->save(changes_only=>1);
264 }) or die "error while transferring sales_delivery_order: " . $sales_delivery_order->db->error;
267 sub transfer_purchase_delivery_order {
268 my ($purchase_delivery_order) = @_;
269 die "first argument must be a purchase delivery order Rose DB object" unless ref($purchase_delivery_order) eq 'SL::DB::DeliveryOrder' and not $purchase_delivery_order->is_sales;
271 my ($wh, $bin, $trans_type);
273 $purchase_delivery_order->db->with_transaction(sub {
275 foreach my $doi ( @{ $purchase_delivery_order->items } ) {
276 my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
277 transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
280 }) or die "error while transferring purchase_Delivery_order: " . $purchase_delivery_order->db->error;
283 sub transfer_delivery_order_item {
284 my ($doi, $wh, $bin, $trans_type) = @_;
286 unless ( defined $trans_type and ref($trans_type eq 'SL::DB::TransferType') ) {
287 if ( $doi->record->is_sales ) {
288 $trans_type //= SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
290 $trans_type //= SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
294 $bin //= $doi->part->bin;
295 $wh //= $doi->part->warehouse;
297 die "no bin and wh specified and part has no default bin or wh" unless $bin and $wh;
299 my $employee = SL::DB::Manager::Employee->current || die "No employee";
301 # dois are converted to base_qty, which is qty
302 # AM->convert_unit( 'g' => 'kg') * 1000; # 1
303 # $doi->unit $doi->part->unit $doi->qty
304 my $dois = SL::DB::DeliveryOrderItemsStock->new(
305 delivery_order_item => $doi,
306 qty => AM->convert_unit($doi->unit => $doi->part->unit) * $doi->qty,
307 unit => $doi->part->unit,
308 warehouse_id => $wh->id,
312 my $inventory = SL::DB::Inventory->new(
313 parts => $dois->delivery_order_item->part,
314 qty => $dois->delivery_order_item->record->is_sales ? $dois->qty * -1 : $dois->qty,
316 warehouse_id => $dois->warehouse_id,
317 bin_id => $dois->bin_id,
318 trans_type_id => $trans_type->id,
319 delivery_order_items_stock => $dois,
320 trans_id => $dois->id,
321 employee_id => $employee->id,
322 shippingdate => $doi->record->transdate,
332 SL::Dev::Inventory - create inventory-related objects for testing, with minimal
337 =head2 C<create_warehouse_and_bins %PARAMS>
339 Creates a new warehouse and bins, and immediately saves them. Returns the
340 warehouse and the first bin object.
341 my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
343 Create named warehouse with 10 bins:
344 my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins(warehouse_description => 'Testlager',
345 bin_description => 'Testlagerplatz',
346 number_of_bins => 10,
348 To access the second bin:
349 my $bin2 = $wh->bins->[1];
351 =head2 C<set_stock %PARAMS>
353 Change the stock level of a certain part by creating an inventory event.
354 To access the updated onhand the part object needs to be loaded afterwards.
357 part - an SL::DB::Part object or a parts_id
359 qty : the qty to increase of decrease the stock level by
360 abs_qty : sets stock level for a certain part to abs_qty by creating
361 a stock event with the current difference
365 shippingdate : may be a DateTime object or a string that needs to be parsed by parse_date_to_object.
366 unit : SL::DB::Unit object, or the name of an SL::DB::Unit object
368 If no bin is passed the default bin of the part is used, if that doesn't exist
369 either there will be an error.
371 C<set_stock> creates the SL::DB::Inventory object from scratch, rather
372 than passing params to WH->transfer_in or WH->transfer_out.
375 my $part = SL::DB::Manager::Part->find_by(partnumber => '1');
376 SL::Dev::Inventory::set_stock(part => $part, qty => 5);
377 SL::Dev::Inventory::set_stock(part => $part, qty => -2);
381 Set stock level of a part in a certain bin_id to 10:
382 SL::Dev::Inventory::set_stock(part => $part, bin_id => 99, abs_qty => 10);
384 Create 10 warehouses with 5 bins each, then create 100 parts and increase the
385 stock qty in a random bin by a random positive qty for each of the parts:
387 SL::Dev::Inventory::create_warehouse_and_bins(warehouse_description => "Testlager $_") for ( 1 .. 10 );
388 SL::Dev::Part::create_part(description => "Testpart $_")->save for ( 1 .. 100 );
389 my $bins = SL::DB::Manager::Bin->get_all;
390 SL::Dev::Inventory::set_stock(part => $_,
391 qty => int(rand(99))+1,
392 bin => $bins->[ rand @{$bins} ],
393 ) foreach @{ SL::DB::Manager::Part->get_all() };
395 =head2 C<transfer_stock %PARAMS>
397 Transfers parts from one bin to another.
400 part | parts_id - an SL::DB::Part object or a parts_id
401 from_bin - an SL::DB::Bin object
402 to_bin qty - an SL::DB::Bin object
404 Optional params: shippingdate
406 The unit is always base_unit and there is no check for negative stock values.
408 Example: Create a warehouse and bins, a part, stock the part and then move some
409 of the stock to a different bin inside the same warehouse:
411 my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
412 my $part = SL::Dev::Part::create_part->save;
413 SL::Dev::Inventory::set_stock(part => $part, bin_id => $wh->bins->[2]->id, qty => 5);
414 SL::Dev::Inventory::transfer_stock(part => $part,
415 from_bin => $wh->bins->[2],
416 to_bin => $wh->bins->[4],
419 $part->get_stock(bin_id => $wh->bins->[4]->id); # 3.00000
420 $part->get_stock(bin_id => $wh->bins->[2]->id); # 2.00000
422 =head2 C<transfer_sales_delivery_order %PARAMS>
424 Takes a SL::DB::DeliveryOrder object as its first argument and transfers out
425 all the items via their default bin, creating the delivery_order_stock and
428 Assumes a fresh delivery order where nothing has been transferred out yet.
430 Should work like the functions in do.pl transfer_in/transfer_out and DO.pm
431 transfer_in_out, except that those work on the current form where as this just
432 works on database objects.
434 As this is just Dev it doesn't check for negative stocks etc.
437 my $sales_delivery_order = SL::DB::Manager::DeliveryOrder->find_by(donumber => 112);
438 SL::Dev::Inventory::transfer_sales_delivery_order($sales_delivery_order1);
440 =head2 C<transfer_purchase_delivery_order %PARAMS>
442 Transfer in all the items in a purchase order.
444 Behaves like C<transfer_sales_delivery_order>.
446 =head2 C<transfer_delivery_order_item @PARAMS>
448 Transfers a delivery order item from a delivery order. The whole qty is transferred.
449 Doesn't check for available qty.
452 SL::Dev::Inventory::transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
454 =head2 C<transfer_in %PARAMS>
456 Create stock in event for a part. Ideally the interface should mirror how data
457 is entered via the web interface.
459 Does some param checking, sets some defaults, but otherwise uses WH->transfer.
462 part - an SL::DB::Part object
465 Optional params: shippingdate
466 bin - an SL::DB::Bin object, defaults to $part->bin
467 wh - an SL::DB::Bin object, defaults to $part->warehouse
468 unit - a string such as 't', 'Stck', defaults to $part->unit->name
469 shippingdate - a DateTime object, defaults to today
470 transfer_type - a string such as 'correction', defaults to 'stock'
473 Example minimal usage using part default warehouse and bin:
474 my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
475 my $part = SL::Dev::Part::create_part(unit => 'kg', warehouse => $wh, bin => $bin)->save;
476 SL::Dev::Inventory::transfer_in(part => $part, qty => '0.9', unit => 't', comment => '900 kg in t');
478 Example with specific transfer_type and warehouse and bin and shipping_date:
479 my $shipping_date = DateTime->today->subtract( days => 20 );
480 SL::Dev::Inventory::transfer_in(part => $part,
482 transfer_type => 'correction',
484 shipping_date => $shipping_date,
487 =head2 C<transfer_out %PARAMS>
489 Create stock out event for a part. See C<transfer_in>.
497 G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>