1 package SL::Dev::Inventory;
 
   6   create_warehouse_and_bins set_stock transfer_stock
 
   7   transfer_sales_delivery_order transfer_purchase_delivery_order
 
   8   transfer_delivery_order_item transfer_in transfer_out
 
  10 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
  12 use SL::DB::Warehouse;
 
  14 use SL::DB::Inventory;
 
  15 use SL::DB::TransferType;
 
  17 use SL::DB::DeliveryOrderItemsStock;
 
  23 sub create_warehouse_and_bins {
 
  26   my $number_of_bins = $params{number_of_bins} || 5;
 
  27   my $wh = SL::DB::Warehouse->new(description => $params{warehouse_description} || "Warehouse", invalid => 0);
 
  28   for my $i ( 1 .. $number_of_bins ) {
 
  29     $wh->add_bins( SL::DB::Bin->new(description => ( $params{bin_description} || "Bin" ) . " $i" ) );
 
  32   return ($wh, $wh->bins->[0]);
 
  38   die "param part is missing or not an SL::DB::Part object" unless ref($params{part}) eq 'SL::DB::Part';
 
  39   my $part = delete $params{part};
 
  40   die "qty is missing" unless $params{qty} or $params{abs_qty};
 
  41   die "need a bin or default bin" unless $part->warehouse_id or $part->bin_id or $params{bin} or $params{bin_id};
 
  43   my ($warehouse_id, $bin_id);
 
  46     die "illegal param bin: " . Dumper($params{bin}) unless ref($params{bin}) eq 'SL::DB::Bin';
 
  47     my $bin       = delete $params{bin};
 
  49     $warehouse_id = $bin->warehouse_id;
 
  50   } elsif ( $params{bin_id} ) {
 
  51     my $bin       = SL::DB::Manager::Bin->find_by(id => delete $params{bin_id});
 
  53     $warehouse_id = $bin->warehouse_id;
 
  54   } elsif ( $part->bin_id ) {
 
  55     $bin_id       = $part->bin_id;
 
  56     $warehouse_id = $part->warehouse_id;
 
  58     die "can't determine bin and warehouse";
 
  61   my $employee_id = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
 
  62   die "Can't determine employee" unless $employee_id;
 
  64   my $qty = delete $params{qty};
 
  66   my $transfer_type_description;
 
  68   if ( $params{abs_qty} ) {
 
  69     # determine the current qty and calculate the qty diff that needs to be applied
 
  70     # if abs_qty is set then any value that was in $params{qty} is ignored/overwritten
 
  72     $get_stock_params{bin_id}       = $bin_id       if $bin_id;
 
  73     # $get_stock_params{warehouse_id} = $warehouse_id if $warehouse_id; # redundant
 
  74     my $current_qty = $part->get_stock(%get_stock_params);
 
  75     $qty = $params{abs_qty} - $current_qty;
 
  79     $transfer_type_description = delete $params{transfer_type} // 'stock';
 
  80     $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'in' );
 
  82     $transfer_type_description = delete $params{transfer_type} // 'shipped';
 
  83     $transfer_type = SL::DB::Manager::TransferType->find_by( description => $transfer_type_description, direction => 'out' );
 
  85   die "can't determine transfer_type" unless $transfer_type;
 
  88   if ( $params{shippingdate} ) {
 
  89     $shippingdate = delete $params{shippingdate};
 
  90     $shippingdate = $::locale->parse_date_to_object($shippingdate) unless ref($shippingdate) eq 'DateTime';
 
  92     $shippingdate = DateTime->today;
 
  96   if ( $params{unit} ) {
 
  97     $unit = delete $params{unit};
 
  98     $unit = SL::DB::Manager::Unit->find_by( name => $unit ) unless ref($unit) eq 'SL::DB::Unit';
 
  99     $qty  = $unit->convert_to($qty, $part->unit_obj);
 
 102   my ($trans_id) = $part->db->dbh->selectrow_array("select nextval('id')", {});
 
 104   SL::DB::Inventory->new(
 
 105     parts_id         => $part->id,
 
 107     warehouse_id     => $warehouse_id,
 
 108     employee_id      => $employee_id,
 
 109     trans_type_id    => $transfer_type->id,
 
 110     comment          => $params{comment},
 
 111     shippingdate     => $shippingdate,
 
 113     trans_id         => $trans_id,
 
 121   die "missing params" unless ( $params{parts_id} or $params{part} ) and $params{from_bin} and $params{to_bin};
 
 124   if ( $params{parts_id} ) {
 
 125     $part = SL::DB::Manager::Part->find_by( id => delete $params{parts_id} ) or die "illegal parts_id";
 
 127     $part = delete $params{part};
 
 129   die "illegal part" unless ref($part) eq 'SL::DB::Part';
 
 131   my $from_bin = delete $params{from_bin};
 
 132   my $to_bin   = delete $params{to_bin};
 
 133   die "illegal bins" unless ref($from_bin) eq 'SL::DB::Bin' and ref($to_bin) eq 'SL::DB::Bin';
 
 135   my $qty = delete($params{qty});
 
 136   die "qty must be > 0" unless $qty > 0;
 
 139   my $transfer_type = SL::DB::Manager::TransferType->find_by(description => 'transfer') or die "can't determine transfer type";
 
 140   my $employee_id   = delete $params{employee_id} // SL::DB::Manager::Employee->current->id;
 
 143     'bestbefore'         => undef,
 
 144     'change_default_bin' => undef,
 
 145     'chargenumber'       => '',
 
 146     'comment'            => delete $params{comment} // '',
 
 147     'dst_bin_id'         => $to_bin->id,
 
 148     'dst_warehouse_id'   => $to_bin->warehouse_id,
 
 149     'parts_id'           => $part->id,
 
 151     'src_bin_id'         => $from_bin->id,
 
 152     'src_warehouse_id'   => $from_bin->warehouse_id,
 
 153     'transfer_type_id'   => $transfer_type->id,
 
 156   WH->transfer($WH_params);
 
 160   # do it manually via rose:
 
 163   # my $db = SL::DB::Inventory->new->db;
 
 164   # $db->with_transaction(sub{
 
 165   #   ($trans_id) = $db->dbh->selectrow_array("select nextval('id')", {});
 
 166   #   die "no trans_id" unless $trans_id;
 
 169   #     shippingdate  => delete $params{shippingdate} // DateTime->today,
 
 170   #     employee_id   => $employee_id,
 
 171   #     trans_id      => $trans_id,
 
 172   #     trans_type_id => $transfer_type->id,
 
 173   #     parts_id      => $part->id,
 
 174   #     comment       => delete $params{comment} || 'Umlagerung',
 
 177   #   SL::DB::Inventory->new(
 
 178   #     warehouse_id => $from_bin->warehouse_id,
 
 179   #     bin_id       => $from_bin->id,
 
 184   #   SL::DB::Inventory->new(
 
 185   #     warehouse_id => $to_bin->warehouse_id,
 
 186   #     bin_id       => $to_bin->id,
 
 190   # }) or die $@ . "\n";
 
 197   my $transfer_type = delete $params{transfer_type};
 
 199   die "param transfer_type is not a SL::DB::TransferType object: " . Dumper($transfer_type) unless ref($transfer_type) eq 'SL::DB::TransferType';
 
 201   my $shippingdate  = delete $params{shippingdate}  // DateTime->today;
 
 203   my $part = delete($params{part}) or croak 'part missing';
 
 204   my $qty  = delete($params{qty})  or croak 'qty missing';
 
 206   # distinguish absolute qty in inventory depending on transfer type direction
 
 207   $qty *= -1 if $transfer_type->direction eq 'out';
 
 209   # use defaults for unit/wh/bin is they exist and nothing else is specified
 
 210   my $unit = delete($params{unit}) // $part->unit      or croak 'unit missing';
 
 211   my $bin  = delete($params{bin})  // $part->bin       or croak 'bin missing';
 
 212   # if bin is given, we don't need a warehouse param
 
 213   my $wh   = $bin->warehouse or croak 'wh missing';
 
 216     parts_id         => $part->id,
 
 220     transfer_type    => $transfer_type,
 
 222     comment          => delete $params{comment},
 
 223     shippingdate     => $shippingdate,
 
 230   my $transfer_type = delete $params{transfer_type} // 'stock';
 
 232   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};
 
 234   $params{transfer_type} = $transfer_type_obj;
 
 242   my $transfer_type = delete $params{transfer_type} // 'shipped';
 
 244   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};
 
 246   $params{transfer_type} = $transfer_type_obj;
 
 251 sub transfer_sales_delivery_order {
 
 252   my ($sales_delivery_order) = @_;
 
 253   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;
 
 255   die "the delivery order has already been delivered" if $sales_delivery_order->delivered;
 
 257   my ($wh, $bin, $trans_type);
 
 259   $sales_delivery_order->db->with_transaction(sub {
 
 261    foreach my $doi ( @{ $sales_delivery_order->items } ) {
 
 262      next if $doi->part->is_service or $doi->part->is_assortment;
 
 263      my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
 
 264      transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
 
 266    $sales_delivery_order->delivered(1);
 
 267    $sales_delivery_order->save(changes_only=>1);
 
 269   }) or die "error while transferring sales_delivery_order: " . $sales_delivery_order->db->error;
 
 272 sub transfer_purchase_delivery_order {
 
 273   my ($purchase_delivery_order) = @_;
 
 274   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;
 
 276   my ($wh, $bin, $trans_type);
 
 278   $purchase_delivery_order->db->with_transaction(sub {
 
 280    foreach my $doi ( @{ $purchase_delivery_order->items } ) {
 
 281      my $trans_type = SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
 
 282      transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
 
 285   }) or die "error while transferring purchase_Delivery_order: " . $purchase_delivery_order->db->error;
 
 288 sub transfer_delivery_order_item {
 
 289   my ($doi, $wh, $bin, $trans_type) = @_;
 
 291   unless ( defined $trans_type and ref($trans_type eq 'SL::DB::TransferType') ) {
 
 292     if ( $doi->record->is_sales ) {
 
 293       $trans_type //=  SL::DB::Manager::TransferType->find_by(direction => 'out', description => 'shipped');
 
 295       $trans_type //= SL::DB::Manager::TransferType->find_by(direction => 'in', description => 'stock');
 
 299   $bin //= $doi->part->bin;
 
 300   $wh  //= $doi->part->warehouse;
 
 302   die "no bin and wh specified and part has no default bin or wh" unless $bin and $wh;
 
 304   my $employee = SL::DB::Manager::Employee->current || die "No employee";
 
 306   # dois are converted to base_qty, which is qty
 
 307   # AM->convert_unit( 'g' => 'kg') * 1000;   # 1
 
 308   #               $doi->unit   $doi->part->unit   $doi->qty
 
 309   my $dois = SL::DB::DeliveryOrderItemsStock->new(
 
 310     delivery_order_item => $doi,
 
 311     qty                 => AM->convert_unit($doi->unit => $doi->part->unit) * $doi->qty,
 
 312     unit                => $doi->part->unit,
 
 313     warehouse_id        => $wh->id,
 
 317   my $inventory = SL::DB::Inventory->new(
 
 318     parts                      => $dois->delivery_order_item->part,
 
 319     qty                        => $dois->delivery_order_item->record->is_sales ? $dois->qty * -1 : $dois->qty,
 
 321     warehouse_id               => $dois->warehouse_id,
 
 322     bin_id                     => $dois->bin_id,
 
 323     trans_type_id              => $trans_type->id,
 
 324     delivery_order_items_stock => $dois,
 
 325     trans_id                   => $dois->id,
 
 326     employee_id                => $employee->id,
 
 327     shippingdate               => $doi->record->transdate,
 
 337 SL::Dev::Inventory - create inventory-related objects for testing, with minimal
 
 342 =head2 C<create_warehouse_and_bins %PARAMS>
 
 344 Creates a new warehouse and bins, and immediately saves them. Returns the
 
 345 warehouse and the first bin object.
 
 347   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 349 Create named warehouse with 10 bins:
 
 351   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins(
 
 352     warehouse_description => 'Test warehouse',
 
 353     bin_description       => 'Test bin',
 
 354     number_of_bins        => 10,
 
 357 To access the second bin:
 
 359   my $bin2 = $wh->bins->[1];
 
 361 =head2 C<set_stock %PARAMS>
 
 363 Change the stock level of a certain part by creating an inventory event.
 
 364 To access the updated onhand the part object needs to be loaded afterwards.
 
 372 Mandatory. An SL::DB::Part object or a parts_id.
 
 376 The qty to increase of decrease the stock level by.
 
 378 Exactly one of C<qty> and C<abs_qty> is mandatory.
 
 382 Sets stock level for a certain part to abs_qty by creating a stock event with
 
 383 the current difference.
 
 385 Exactly one of C<qty> and C<abs_qty> is mandatory.
 
 391 Optional. The bin for inventory entry.
 
 393 If no bin is passed the default bin of the part is used, if that doesn't exist
 
 394 either there will be an error.
 
 396 =item C<shippingdate>
 
 398 Optional. May be a DateTime object or a string that needs to be parsed by
 
 399 parse_date_to_object.
 
 403 Optional. SL::DB::Unit object, or the name of an SL::DB::Unit object.
 
 407 C<set_stock> creates the SL::DB::Inventory object from scratch, rather
 
 408 than passing params to WH->transfer_in or WH->transfer_out.
 
 412   my $part = SL::DB::Manager::Part->find_by(partnumber => '1');
 
 413   SL::Dev::Inventory::set_stock(part => $part, abs_qty => 5);
 
 414   SL::Dev::Inventory::set_stock(part => $part, qty => -2);
 
 418 Set stock level of a part in a certain bin_id to 10:
 
 420   SL::Dev::Inventory::set_stock(part => $part, bin_id => 99, abs_qty => 10);
 
 422 Create 10 warehouses with 5 bins each, then create 100 parts and increase the
 
 423 stock qty in a random bin by a random positive qty for each of the parts:
 
 425   SL::Dev::Inventory::create_warehouse_and_bins(
 
 426     warehouse_description => "Test Warehouse $_"
 
 428   SL::Dev::Part::create_part(
 
 429     description => "Test Part $_"
 
 430   )->save for 1 .. 100;
 
 431   my $bins = SL::DB::Manager::Bin->get_all;
 
 432   SL::Dev::Inventory::set_stock(
 
 434     qty  => int(rand(99))+1,
 
 435     bin  => $bins->[ rand @{$bins} ],
 
 436   ) for @{ SL::DB::Manager::Part->get_all };
 
 438 =head2 C<transfer_stock %PARAMS>
 
 440 Transfers parts from one bin to another.
 
 450 Mandatory. An SL::DB::Part object or a parts_id.
 
 456 Mandatory. SL::DB::Bin objects.
 
 462 =item C<shippingdate>
 
 468 The unit is always base_unit and there is no check for negative stock values.
 
 470 Example: Create a warehouse and bins, a part, stock the part and then move some
 
 471 of the stock to a different bin inside the same warehouse:
 
 473   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 474   my $part = SL::Dev::Part::create_part->save;
 
 475   SL::Dev::Inventory::set_stock(
 
 477     bin_id => $wh->bins->[2]->id,
 
 480   SL::Dev::Inventory::transfer_stock(
 
 482     from_bin => $wh->bins->[2],
 
 483     to_bin   => $wh->bins->[4],
 
 486   $part->get_stock(bin_id => $wh->bins->[4]->id); # 3.00000
 
 487   $part->get_stock(bin_id => $wh->bins->[2]->id); # 2.00000
 
 489 =head2 C<transfer_sales_delivery_order %PARAMS>
 
 491 Takes a SL::DB::DeliveryOrder object as its first argument and transfers out
 
 492 all the items via their default bin, creating the delivery_order_stock and
 
 495 Assumes a fresh delivery order where nothing has been transferred out yet.
 
 497 Should work like the functions in do.pl transfer_in/transfer_out and DO.pm
 
 498 transfer_in_out, except that those work on the current form where as this just
 
 499 works on database objects.
 
 501 As this is just Dev it doesn't check for negative stocks etc.
 
 505   my $sales_delivery_order = SL::DB::Manager::DeliveryOrder->find_by(donumber => 112);
 
 506   SL::Dev::Inventory::transfer_sales_delivery_order($sales_delivery_order1);
 
 508 =head2 C<transfer_purchase_delivery_order %PARAMS>
 
 510 Transfer in all the items in a purchase order.
 
 512 Behaves like C<transfer_sales_delivery_order>.
 
 514 =head2 C<transfer_delivery_order_item @PARAMS>
 
 516 Transfers a delivery order item from a delivery order. The whole qty is transferred.
 
 517 Doesn't check for available qty.
 
 521   SL::Dev::Inventory::transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
 
 523 =head2 C<transfer_in %PARAMS>
 
 525 Create stock in event for a part. Ideally the interface should mirror how data
 
 526 is entered via the web interface.
 
 528 Does some param checking, sets some defaults, but otherwise uses WH->transfer.
 
 536 Mandatory. An SL::DB::Part object.
 
 544 Optional. An SL::DB::Bin object, defaults to $part->bin.
 
 548 Optional. An SL::DB::Bin object, defaults to $part->warehouse.
 
 552 Optional. A string such as 't', 'Stck', defaults to $part->unit->name.
 
 554 =item C<shippingdate>
 
 556 Optional. A DateTime object, defaults to today.
 
 558 =item C<transfer_type>
 
 560 Optional. A string such as 'correction', defaults to 'stock'.
 
 568 Example minimal usage using part default warehouse and bin:
 
 570   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 571   my $part       = SL::Dev::Part::create_part(
 
 576   SL::Dev::Inventory::transfer_in(
 
 580     comment => '900 kg in t',
 
 583 Example with specific transfer_type and warehouse and bin and shipping_date:
 
 585   my $shipping_date = DateTime->today->subtract( days => 20 );
 
 586   SL::Dev::Inventory::transfer_in(
 
 589     transfer_type => 'correction',
 
 591     shipping_date => $shipping_date,
 
 594 =head2 C<transfer_out %PARAMS>
 
 596 Create stock out event for a part. See C<transfer_in>.
 
 604 G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>