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.
 
 342   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 344 Create named warehouse with 10 bins:
 
 346   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins(
 
 347     warehouse_description => 'Test warehouse',
 
 348     bin_description       => 'Test bin',
 
 349     number_of_bins        => 10,
 
 352 To access the second bin:
 
 354   my $bin2 = $wh->bins->[1];
 
 356 =head2 C<set_stock %PARAMS>
 
 358 Change the stock level of a certain part by creating an inventory event.
 
 359 To access the updated onhand the part object needs to be loaded afterwards.
 
 367 Mandatory. An SL::DB::Part object or a parts_id.
 
 371 The qty to increase of decrease the stock level by.
 
 373 Exactly one of C<qty> and C<abs_qty> is mandatory.
 
 377 Sets stock level for a certain part to abs_qty by creating a stock event with
 
 378 the current difference.
 
 380 Exactly one of C<qty> and C<abs_qty> is mandatory.
 
 386 Optional. The bin for inventory entry.
 
 388 If no bin is passed the default bin of the part is used, if that doesn't exist
 
 389 either there will be an error.
 
 391 =item C<shippingdate>
 
 393 Optional. May be a DateTime object or a string that needs to be parsed by
 
 394 parse_date_to_object.
 
 398 Optional. SL::DB::Unit object, or the name of an SL::DB::Unit object.
 
 402 C<set_stock> creates the SL::DB::Inventory object from scratch, rather
 
 403 than passing params to WH->transfer_in or WH->transfer_out.
 
 407   my $part = SL::DB::Manager::Part->find_by(partnumber => '1');
 
 408   SL::Dev::Inventory::set_stock(part => $part, abs_qty => 5);
 
 409   SL::Dev::Inventory::set_stock(part => $part, qty => -2);
 
 413 Set stock level of a part in a certain bin_id to 10:
 
 415   SL::Dev::Inventory::set_stock(part => $part, bin_id => 99, abs_qty => 10);
 
 417 Create 10 warehouses with 5 bins each, then create 100 parts and increase the
 
 418 stock qty in a random bin by a random positive qty for each of the parts:
 
 420   SL::Dev::Inventory::create_warehouse_and_bins(
 
 421     warehouse_description => "Test Warehouse $_"
 
 423   SL::Dev::Part::create_part(
 
 424     description => "Test Part $_"
 
 425   )->save for 1 .. 100;
 
 426   my $bins = SL::DB::Manager::Bin->get_all;
 
 427   SL::Dev::Inventory::set_stock(
 
 429     qty  => int(rand(99))+1,
 
 430     bin  => $bins->[ rand @{$bins} ],
 
 431   ) for @{ SL::DB::Manager::Part->get_all };
 
 433 =head2 C<transfer_stock %PARAMS>
 
 435 Transfers parts from one bin to another.
 
 445 Mandatory. An SL::DB::Part object or a parts_id.
 
 451 Mandatory. SL::DB::Bin objects.
 
 457 =item C<shippingdate>
 
 463 The unit is always base_unit and there is no check for negative stock values.
 
 465 Example: Create a warehouse and bins, a part, stock the part and then move some
 
 466 of the stock to a different bin inside the same warehouse:
 
 468   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 469   my $part = SL::Dev::Part::create_part->save;
 
 470   SL::Dev::Inventory::set_stock(
 
 472     bin_id => $wh->bins->[2]->id,
 
 475   SL::Dev::Inventory::transfer_stock(
 
 477     from_bin => $wh->bins->[2],
 
 478     to_bin   => $wh->bins->[4],
 
 481   $part->get_stock(bin_id => $wh->bins->[4]->id); # 3.00000
 
 482   $part->get_stock(bin_id => $wh->bins->[2]->id); # 2.00000
 
 484 =head2 C<transfer_sales_delivery_order %PARAMS>
 
 486 Takes a SL::DB::DeliveryOrder object as its first argument and transfers out
 
 487 all the items via their default bin, creating the delivery_order_stock and
 
 490 Assumes a fresh delivery order where nothing has been transferred out yet.
 
 492 Should work like the functions in do.pl transfer_in/transfer_out and DO.pm
 
 493 transfer_in_out, except that those work on the current form where as this just
 
 494 works on database objects.
 
 496 As this is just Dev it doesn't check for negative stocks etc.
 
 500   my $sales_delivery_order = SL::DB::Manager::DeliveryOrder->find_by(donumber => 112);
 
 501   SL::Dev::Inventory::transfer_sales_delivery_order($sales_delivery_order1);
 
 503 =head2 C<transfer_purchase_delivery_order %PARAMS>
 
 505 Transfer in all the items in a purchase order.
 
 507 Behaves like C<transfer_sales_delivery_order>.
 
 509 =head2 C<transfer_delivery_order_item @PARAMS>
 
 511 Transfers a delivery order item from a delivery order. The whole qty is transferred.
 
 512 Doesn't check for available qty.
 
 516   SL::Dev::Inventory::transfer_delivery_order_item($doi, $wh, $bin, $trans_type);
 
 518 =head2 C<transfer_in %PARAMS>
 
 520 Create stock in event for a part. Ideally the interface should mirror how data
 
 521 is entered via the web interface.
 
 523 Does some param checking, sets some defaults, but otherwise uses WH->transfer.
 
 531 Mandatory. An SL::DB::Part object.
 
 539 Optional. An SL::DB::Bin object, defaults to $part->bin.
 
 543 Optional. An SL::DB::Bin object, defaults to $part->warehouse.
 
 547 Optional. A string such as 't', 'Stck', defaults to $part->unit->name.
 
 549 =item C<shippingdate>
 
 551 Optional. A DateTime object, defaults to today.
 
 553 =item C<transfer_type>
 
 555 Optional. A string such as 'correction', defaults to 'stock'.
 
 563 Example minimal usage using part default warehouse and bin:
 
 565   my ($wh, $bin) = SL::Dev::Inventory::create_warehouse_and_bins();
 
 566   my $part       = SL::Dev::Part::create_part(
 
 571   SL::Dev::Inventory::transfer_in(
 
 575     comment => '900 kg in t',
 
 578 Example with specific transfer_type and warehouse and bin and shipping_date:
 
 580   my $shipping_date = DateTime->today->subtract( days => 20 );
 
 581   SL::Dev::Inventory::transfer_in(
 
 584     transfer_type => 'correction',
 
 586     shipping_date => $shipping_date,
 
 589 =head2 C<transfer_out %PARAMS>
 
 591 Create stock out event for a part. See C<transfer_in>.
 
 599 G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>