Anzahlungs-Rg.: Druckvariblen: offenen Betrag jeder Anzahlungs-Rg. und Summe
[kivitendo-erp.git] / t / wh / inventory.t
1 use strict;
2 use Test::Deep qw(cmp_deeply ignore superhashof);
3 use Test::More;
4 use Test::Exception;
5
6 use lib 't';
7
8 use SL::Dev::Part qw(new_part new_assembly new_service);
9 use SL::Dev::Inventory qw(create_warehouse_and_bins set_stock);
10 use SL::Dev::Record qw(create_sales_order);
11
12 use_ok 'Support::TestSetup';
13 use_ok 'SL::DB::Bin';
14 use_ok 'SL::DB::Part';
15 use_ok 'SL::DB::Warehouse';
16 use_ok 'SL::DB::Inventory';
17 use_ok 'SL::WH';
18 use_ok 'SL::Helper::Inventory';
19
20 Support::TestSetup::login();
21
22 my ($wh, $bin1, $bin2, $assembly1, $assembly_service, $part1, $part2, $wh_moon, $bin_moon, $service1);
23 my @contents;
24
25 reset_db();
26 create_standard_stock();
27
28
29 # simple stock in, get_stock, get_onhand
30 set_stock(
31   part => $part1,
32   qty => 25,
33   bin => $bin1,
34 );
35
36 is(SL::Helper::Inventory::get_stock(part => $part1), "25.00000", 'simple get_stock works');
37 is(SL::Helper::Inventory::get_onhand(part => $part1), "25.00000", 'simple get_onhand works');
38
39 # stock on some more, get_stock, get_onhand
40
41 WH->transfer({
42   parts_id          => $part1->id,
43   qty               => 15,
44   transfer_type     => 'stock',
45   dst_warehouse_id  => $bin1->warehouse_id,
46   dst_bin_id        => $bin1->id,
47   comment           => 'more',
48 });
49
50 WH->transfer({
51   parts_id          => $part1->id,
52   qty               => 20,
53   transfer_type     => 'stock',
54   chargenumber      => '298345',
55   dst_warehouse_id  => $bin1->warehouse_id,
56   dst_bin_id        => $bin1->id,
57   comment           => 'more',
58 });
59
60 is(SL::Helper::Inventory::get_stock(part => $part1), "60.00000", 'normal get_stock works');
61 is(SL::Helper::Inventory::get_onhand(part => $part1), "60.00000", 'normal get_onhand works');
62
63 # allocate some stuff
64
65 my @allocations = SL::Helper::Inventory::allocate(
66   part => $part1,
67   qty  => 12,
68 );
69
70 is_deeply(\%{ $allocations[0] }, {
71    bestbefore        => undef,
72    bin_id            => $bin1->id,
73    chargenumber      => '',
74    parts_id          => $part1->id,
75    qty               => 12,
76    warehouse_id      => $wh->id,
77    comment           => undef, # comment is not a partition so is not set by allocate
78    for_object_id     => undef,
79  }, 'allocation works');
80
81 # allocate something where more than one result will match
82
83 @allocations = SL::Helper::Inventory::allocate(
84   part => $part1,
85   qty  => 55,
86 );
87
88 is_deeply(\@allocations, [
89   {
90     bestbefore        => undef,
91     bin_id            => $bin1->id,
92     chargenumber      => '',
93     parts_id          => $part1->id,
94     qty               => '40.00000',
95     warehouse_id      => $wh->id,
96     comment           => undef,
97     for_object_id     => undef,
98   },
99   {
100     bestbefore        => undef,
101     bin_id            => $bin1->id,
102     chargenumber      => '298345',
103     parts_id          => $part1->id,
104     qty               => '15',
105     warehouse_id      => $wh->id,
106     comment           => undef,
107     for_object_id     => undef,
108   }
109 ], 'complex allocation works');
110
111 # try to allocate too much
112
113 dies_ok(sub {
114   SL::Helper::Inventory::allocate(part => $part1, qty => 100)
115 },
116 "allocate too much dies");
117
118 # produce something
119
120 reset_db();
121 create_standard_stock();
122
123 set_stock(
124   part => $part1,
125   qty => 5,
126   bin => $bin1,
127 );
128 set_stock(
129   part => $part2,
130   qty => 10,
131   bin => $bin1,
132 );
133
134
135 my @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 3);
136 my @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 3);
137
138 SL::Helper::Inventory::produce_assembly(
139   part          => $assembly1,
140   qty           => 3,
141   allocations => [ @alloc1, @alloc2 ],
142
143   # where to put it
144   bin          => $bin1,
145   chargenumber => "537",
146 );
147
148 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce works');
149 is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
150 is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
151
152 # produce the same using auto_allocation
153
154 local $::locale = Locale->new('en');
155 reset_db();
156 create_standard_stock();
157
158 set_stock(
159   part => $part1,
160   qty => 5,
161   bin => $bin1,
162 );
163 set_stock(
164   part => $part2,
165   qty => 10,
166   bin => $bin1,
167 );
168
169 SL::Helper::Inventory::produce_assembly(
170   part          => $assembly1,
171   qty           => 3,
172   auto_allocate => 1,
173
174   # where to put it
175   bin          => $bin1,
176   chargenumber => "537",
177 );
178
179 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with auto allocation works');
180 is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
181 is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
182
183 # check comments and warehouses
184 $::form->{l_comment}        = 'Y';
185 $::form->{l_warehouse_from} = 'Y';
186 $::form->{l_warehouse_to}   = 'Y';
187 local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
188
189 @contents = WH->get_warehouse_journal(sort => 'date');
190
191 cmp_deeply(\@contents,
192            [ ignore(), ignore(),
193               superhashof({
194                 'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
195                 'warehouse_from' => 'Warehouse'
196               }),
197               superhashof({
198                 'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
199                 'warehouse_from' => 'Warehouse'
200               }),
201               superhashof({
202                 'part_type'    => 'assembly',
203                 'warehouse_to' => 'Warehouse'
204               }),
205            ],
206           "Comments for assembly productions are ok"
207 );
208
209 # try to produce something for our lunar warehouse, but parts are only available on earth
210 dies_ok(sub {
211 SL::Helper::Inventory::produce_assembly(
212   part          => $assembly1,
213   qty           => 1,
214   auto_allocate => 1,
215   # where to put it
216   bin          => $bin_moon,
217   chargenumber => "Lunar Dust inside",
218 );
219 }, "producing for wrong warehouse dies");
220
221 # same test, but check exception class
222 throws_ok{
223 SL::Helper::Inventory::produce_assembly(
224   part          => $assembly1,
225   qty           => 1,
226   auto_allocate => 1,
227   # where to put it
228   bin          => $bin_moon,
229   chargenumber => "Lunar Dust inside",
230 );
231  } "SL::X::Inventory::Allocation", "producing for wrong warehouse throws correct error class";
232
233 # same test, but check user feedback for the error message
234 throws_ok{
235 SL::Helper::Inventory::produce_assembly(
236   part          => $assembly1,
237   qty           => 1,
238   auto_allocate => 1,
239   # where to put it
240   bin          => $bin_moon,
241   chargenumber => "Lunar Dust inside",
242 );
243  } qr/Part ap (1|2) Testpart (1|2) exists in warehouse Warehouse, but not in warehouse Our warehouse location at the moon/, "producing for wrong warehouse throws correct error message";
244
245 # try to produce without allocations dies
246
247 dies_ok(sub {
248 SL::Helper::Inventory::produce_assembly(
249   part          => $assembly1,
250   qty           => 3,
251
252   # where to put it
253   bin          => $bin1,
254   chargenumber => "537",
255 );
256 }, "producing without allocations dies");
257
258 # try to produce with insufficient allocations dies
259
260 @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 1);
261 @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 1);
262
263 dies_ok(sub {
264 SL::Helper::Inventory::produce_assembly(
265   part          => $assembly1,
266   qty           => 3,
267   allocations => [ @alloc1, @alloc2 ],
268
269   # where to put it
270   bin          => $bin1,
271   chargenumber => "537",
272 );
273 }, "producing with insufficient allocations dies");
274
275
276 # assembly with service default tests (services won't be consumed)
277
278 local $::locale = Locale->new('en');
279 reset_db();
280 create_standard_stock();
281
282 set_stock(
283   part => $part1,
284   qty => 12,
285   bin => $bin2,
286 );
287 set_stock(
288   part => $part2,
289   qty => 6.34,
290   bin => $bin2,
291 );
292
293 SL::Helper::Inventory::produce_assembly(
294   part          => $assembly_service,
295   qty           => 1,
296   auto_allocate => 1,
297   # where to put it
298   bin          => $bin1,
299 );
300
301 is(SL::Helper::Inventory::get_stock(part => $assembly_service), "1.00000", 'produce with auto allocation works');
302 is(SL::Helper::Inventory::get_stock(part => $part1), "0.00000", 'and consumes...');
303 is(SL::Helper::Inventory::get_stock(part => $part2), "0.00000", '..the materials');
304
305 # check comments and warehouses
306 $::form->{l_comment}        = 'Y';
307 $::form->{l_warehouse_from} = 'Y';
308 $::form->{l_warehouse_to}   = 'Y';
309 local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
310
311 @contents = WH->get_warehouse_journal(sort => 'date');
312
313 cmp_deeply(\@contents,
314            [ ignore(), ignore(),
315               superhashof({
316                 'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
317                 'warehouse_from' => 'Warehouse'
318               }),
319               superhashof({
320                 'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
321                 'warehouse_from' => 'Warehouse'
322               }),
323               superhashof({
324                 'part_type'    => 'assembly',
325                 'warehouse_to' => 'Warehouse'
326               }),
327            ],
328           "Comments for assembly with service productions are ok"
329 );
330
331 # assembly with service non default tests (services will be consumed)
332
333 local $::instance_conf->data->{produce_assembly_transfer_service} = 1;
334
335 set_stock(
336   part => $part1,
337   qty => 12,
338   bin => $bin2,
339 );
340 set_stock(
341   part => $part2,
342   qty => 6.34,
343   bin => $bin2,
344 );
345
346 throws_ok{
347   SL::Helper::Inventory::produce_assembly(
348     part          => $assembly_service,
349     qty           => 1,
350     auto_allocate => 1,
351     # where to put it
352     bin          => $bin1,
353   );
354 } qr/can not allocate 1,2 units of service number 1 We really need this service, missing 1,2 units/, "producing assembly with services and unstocked service throws correct error message";
355
356 is(SL::Helper::Inventory::get_stock(part => $assembly_service), "1.00000", 'produce without service does not work');
357 is(SL::Helper::Inventory::get_stock(part => $part1), "12.00000", 'and does not consume...');
358 is(SL::Helper::Inventory::get_stock(part => $part2), "6.34000", '..the materials');
359
360
361 # ok, now add the missing service
362 is('SL::DB::Part', ref $service1);
363 set_stock(
364   part => $service1,
365   qty => 1.2,
366   bin => $bin2,
367 );
368
369 SL::Helper::Inventory::produce_assembly(
370   part          => $assembly_service,
371   qty           => 1,
372   auto_allocate => 1,
373   # where to put it
374   bin          => $bin1,
375 );
376
377 is(SL::Helper::Inventory::get_stock(part => $assembly_service), "2.00000", 'produce with service does work if services is needed and stocked');
378 is(SL::Helper::Inventory::get_stock(part => $part1), "0.00000", 'and does consume...');
379 is(SL::Helper::Inventory::get_stock(part => $part2), "0.00000", '..the materials');
380 is(SL::Helper::Inventory::get_stock(part => $service1), "0.00000", '..and service');
381
382 # check comments and warehouses for assembly with service
383 $::form->{l_comment}        = 'Y';
384 $::form->{l_warehouse_from} = 'Y';
385 $::form->{l_warehouse_to}   = 'Y';
386 local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
387
388 @contents = WH->get_warehouse_journal(sort => 'date');
389 #use Data::Dumper;
390 #diag("hier" . Dumper(@contents));
391 cmp_deeply(\@contents,
392          [ ignore(), ignore(), ignore(), ignore(), ignore(), ignore(), ignore(), ignore(),
393               superhashof({
394                 'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
395                 'warehouse_from' => 'Warehouse'
396               }),
397               superhashof({
398                 'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
399                 'warehouse_from' => 'Warehouse'
400               }),
401               superhashof({
402                 'comment'        => 'Used for assembly '. $assembly_service->partnumber .' Ein Erzeugnis mit Dienstleistungen',
403                 'warehouse_from' => 'Warehouse',
404                 'part_type'      => 'service',
405                 'qty'            => '1.20000',
406               }),
407               superhashof({
408                 'part_type'    => 'assembly',
409                 'warehouse_to' => 'Warehouse'
410               }),
411            ],
412           "Comments for assembly with service productions are ok"
413 );
414
415
416
417 # bestbefore tests
418
419 reset_db();
420 create_standard_stock();
421
422 set_stock(
423   part => $part1,
424   qty => 5,
425   bin => $bin1,
426 );
427 set_stock(
428   part => $part2,
429   qty => 10,
430   bin => $bin1,
431 );
432
433
434
435 SL::Helper::Inventory::produce_assembly(
436   part          => $assembly1,
437   qty           => 3,
438   auto_allocate => 1,
439
440   bin               => $bin1,
441   chargenumber      => "537",
442   bestbefore        => DateTime->today->clone->add(days => -14), # expired 2 weeks ago
443   shippingdate      => DateTime->today->clone->add(days => 1),
444 );
445
446 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with bestbefore works');
447 is(SL::Helper::Inventory::get_onhand(part => $assembly1), "3.00000", 'produce with bestbefore works');
448 is(SL::Helper::Inventory::get_stock(
449   part       => $assembly1,
450   bestbefore => DateTime->today,
451 ), undef, 'get_stock with bestbefore date skips expired');
452 {
453   local $::instance_conf->data->{show_bestbefore} = 1;
454   is(SL::Helper::Inventory::get_onhand(
455     part       => $assembly1,
456   ), undef, 'get_onhand with bestbefore skips expired as of today');
457 }
458
459 {
460   local $::instance_conf->data->{show_bestbefore} = 0;
461   is(SL::Helper::Inventory::get_onhand(
462     part       => $assembly1,
463   ), "3.00000", 'get_onhand without bestbefore finds all');
464 }
465
466
467 sub reset_db {
468   SL::DB::Manager::Order->delete_all(all => 1);
469   SL::DB::Manager::Inventory->delete_all(all => 1);
470   SL::DB::Manager::Assembly->delete_all(all => 1);
471   SL::DB::Manager::Part->delete_all(all => 1);
472   SL::DB::Manager::Bin->delete_all(all => 1);
473   SL::DB::Manager::Warehouse->delete_all(all => 1);
474 }
475
476 sub create_standard_stock {
477   ($wh, $bin1)          = create_warehouse_and_bins();
478   ($wh_moon, $bin_moon) = create_warehouse_and_bins(
479       warehouse_description => 'Our warehouse location at the moon',
480       bin_description       => 'Lunar crater',
481     );
482   $bin2 = SL::DB::Bin->new(description => "Bin 2", warehouse => $wh)->save;
483   $wh->load;
484
485   $assembly1  =  new_assembly(number_of_parts => 2)->save;
486   ($part1, $part2) = map { $_->part } $assembly1->assemblies;
487
488   $service1 = new_service(partnumber  => "service number 1",
489                           description => "We really need this service",
490                          )->save;
491   my $assembly_items;
492   push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part1->id,
493                                                   qty      => 12,
494                                                   position => 1,
495                                                   ));
496   push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part2->id,
497                                                   qty      => 6.34,
498                                                   position => 2,
499                                                   ));
500   push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $service1->id,
501                                                   qty      => 1.2,
502                                                   position => 3,
503                                                   ));
504   $assembly_service  =  new_assembly(description    => 'Ein Erzeugnis mit Dienstleistungen',
505                                      assembly_items => $assembly_items
506                                     )->save;
507 }
508
509
510 reset_db();
511
512 done_testing();
513
514 1;