554d34ee33df4c50ed34ade0676c764f30304e25
[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);
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, $part1, $part2, $wh_moon, $bin_moon);
23
24 reset_db();
25 create_standard_stock();
26
27
28 # simple stock in, get_stock, get_onhand
29 set_stock(
30   part => $part1,
31   qty => 25,
32   bin => $bin1,
33 );
34
35 is(SL::Helper::Inventory::get_stock(part => $part1), "25.00000", 'simple get_stock works');
36 is(SL::Helper::Inventory::get_onhand(part => $part1), "25.00000", 'simple get_onhand works');
37
38 # stock on some more, get_stock, get_onhand
39
40 WH->transfer({
41   parts_id          => $part1->id,
42   qty               => 15,
43   transfer_type     => 'stock',
44   dst_warehouse_id  => $bin1->warehouse_id,
45   dst_bin_id        => $bin1->id,
46   comment           => 'more',
47 });
48
49 WH->transfer({
50   parts_id          => $part1->id,
51   qty               => 20,
52   transfer_type     => 'stock',
53   chargenumber      => '298345',
54   dst_warehouse_id  => $bin1->warehouse_id,
55   dst_bin_id        => $bin1->id,
56   comment           => 'more',
57 });
58
59 is(SL::Helper::Inventory::get_stock(part => $part1), "60.00000", 'normal get_stock works');
60 is(SL::Helper::Inventory::get_onhand(part => $part1), "60.00000", 'normal get_onhand works');
61
62 # allocate some stuff
63
64 my @allocations = SL::Helper::Inventory::allocate(
65   part => $part1,
66   qty  => 12,
67 );
68
69 is_deeply(\%{ $allocations[0] }, {
70    bestbefore        => undef,
71    bin_id            => $bin1->id,
72    chargenumber      => '',
73    parts_id          => $part1->id,
74    qty               => 12,
75    warehouse_id      => $wh->id,
76    comment           => undef, # comment is not a partition so is not set by allocate
77    for_object_id     => undef,
78  }, 'allocation works');
79
80 # allocate something where more than one result will match
81
82 @allocations = SL::Helper::Inventory::allocate(
83   part => $part1,
84   qty  => 55,
85 );
86
87 is_deeply(\@allocations, [
88   {
89     bestbefore        => undef,
90     bin_id            => $bin1->id,
91     chargenumber      => '',
92     parts_id          => $part1->id,
93     qty               => '40.00000',
94     warehouse_id      => $wh->id,
95     comment           => undef,
96     for_object_id     => undef,
97   },
98   {
99     bestbefore        => undef,
100     bin_id            => $bin1->id,
101     chargenumber      => '298345',
102     parts_id          => $part1->id,
103     qty               => '15',
104     warehouse_id      => $wh->id,
105     comment           => undef,
106     for_object_id     => undef,
107   }
108 ], 'complex allocation works');
109
110 # try to allocate too much
111
112 dies_ok(sub {
113   SL::Helper::Inventory::allocate(part => $part1, qty => 100)
114 },
115 "allocate too much dies");
116
117 # produce something
118
119 reset_db();
120 create_standard_stock();
121
122 set_stock(
123   part => $part1,
124   qty => 5,
125   bin => $bin1,
126 );
127 set_stock(
128   part => $part2,
129   qty => 10,
130   bin => $bin1,
131 );
132
133
134 my @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 3);
135 my @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 3);
136
137 SL::Helper::Inventory::produce_assembly(
138   part          => $assembly1,
139   qty           => 3,
140   allocations => [ @alloc1, @alloc2 ],
141
142   # where to put it
143   bin          => $bin1,
144   chargenumber => "537",
145 );
146
147 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce works');
148 is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
149 is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
150
151 # produce the same using auto_allocation
152
153 local $::locale = Locale->new('en');
154 reset_db();
155 create_standard_stock();
156
157 set_stock(
158   part => $part1,
159   qty => 5,
160   bin => $bin1,
161 );
162 set_stock(
163   part => $part2,
164   qty => 10,
165   bin => $bin1,
166 );
167
168 SL::Helper::Inventory::produce_assembly(
169   part          => $assembly1,
170   qty           => 3,
171   auto_allocate => 1,
172
173   # where to put it
174   bin          => $bin1,
175   chargenumber => "537",
176 );
177
178 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with auto allocation works');
179 is(SL::Helper::Inventory::get_stock(part => $part1), "2.00000", 'and consumes...');
180 is(SL::Helper::Inventory::get_stock(part => $part2), "7.00000", '..the materials');
181
182 # check comments and warehouses
183 $::form->{l_comment}        = 'Y';
184 $::form->{l_warehouse_from} = 'Y';
185 $::form->{l_warehouse_to}   = 'Y';
186 local $::instance_conf->data->{produce_assembly_same_warehouse} = 1;
187
188 my @contents = WH->get_warehouse_journal(sort => 'date');
189
190 cmp_deeply(\@contents,
191            [ ignore(), ignore(),
192               superhashof({
193                 'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
194                 'warehouse_from' => 'Warehouse'
195               }),
196               superhashof({
197                 'comment'        => 'Used for assembly '. $assembly1->partnumber .' Test Assembly',
198                 'warehouse_from' => 'Warehouse'
199               }),
200               superhashof({
201                 'part_type'    => 'assembly',
202                 'warehouse_to' => 'Warehouse'
203               }),
204            ],
205           "Comments for assembly productions are ok"
206 );
207
208
209
210
211
212
213 # try to produce something for our lunar warehouse, but parts are only available on earth
214 dies_ok(sub {
215 SL::Helper::Inventory::produce_assembly(
216   part          => $assembly1,
217   qty           => 1,
218   auto_allocate => 1,
219   # where to put it
220   bin          => $bin_moon,
221   chargenumber => "Lunar Dust inside",
222 );
223 }, "producing for wrong warehouse dies");
224
225 # same test, but check exception class
226 throws_ok{
227 SL::Helper::Inventory::produce_assembly(
228   part          => $assembly1,
229   qty           => 1,
230   auto_allocate => 1,
231   # where to put it
232   bin          => $bin_moon,
233   chargenumber => "Lunar Dust inside",
234 );
235  } "SL::X::Inventory::Allocation", "producing for wrong warehouse throws correct error class";
236
237 # same test, but check user feedback for the error message
238 throws_ok{
239 SL::Helper::Inventory::produce_assembly(
240   part          => $assembly1,
241   qty           => 1,
242   auto_allocate => 1,
243   # where to put it
244   bin          => $bin_moon,
245   chargenumber => "Lunar Dust inside",
246 );
247  } 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";
248
249 # try to produce without allocations dies
250
251 dies_ok(sub {
252 SL::Helper::Inventory::produce_assembly(
253   part          => $assembly1,
254   qty           => 3,
255
256   # where to put it
257   bin          => $bin1,
258   chargenumber => "537",
259 );
260 }, "producing without allocations dies");
261
262 # try to produce with insufficient allocations dies
263
264 @alloc1 = SL::Helper::Inventory::allocate(part => $part1, qty => 1);
265 @alloc2 = SL::Helper::Inventory::allocate(part => $part2, qty => 1);
266
267 dies_ok(sub {
268 SL::Helper::Inventory::produce_assembly(
269   part          => $assembly1,
270   qty           => 3,
271   allocations => [ @alloc1, @alloc2 ],
272
273   # where to put it
274   bin          => $bin1,
275   chargenumber => "537",
276 );
277 }, "producing with insufficient allocations dies");
278
279
280
281 # bestbefore tests
282
283 reset_db();
284 create_standard_stock();
285
286 set_stock(
287   part => $part1,
288   qty => 5,
289   bin => $bin1,
290 );
291 set_stock(
292   part => $part2,
293   qty => 10,
294   bin => $bin1,
295 );
296
297
298
299 SL::Helper::Inventory::produce_assembly(
300   part          => $assembly1,
301   qty           => 3,
302   auto_allocate => 1,
303
304   bin               => $bin1,
305   chargenumber      => "537",
306   bestbefore        => DateTime->today->clone->add(days => -14), # expired 2 weeks ago
307   shippingdate      => DateTime->today->clone->add(days => 1),
308 );
309
310 is(SL::Helper::Inventory::get_stock(part => $assembly1), "3.00000", 'produce with bestbefore works');
311 is(SL::Helper::Inventory::get_onhand(part => $assembly1), "3.00000", 'produce with bestbefore works');
312 is(SL::Helper::Inventory::get_stock(
313   part       => $assembly1,
314   bestbefore => DateTime->today,
315 ), undef, 'get_stock with bestbefore date skips expired');
316 {
317   local $::instance_conf->data->{show_bestbefore} = 1;
318   is(SL::Helper::Inventory::get_onhand(
319     part       => $assembly1,
320   ), undef, 'get_onhand with bestbefore skips expired as of today');
321 }
322
323 {
324   local $::instance_conf->data->{show_bestbefore} = 0;
325   is(SL::Helper::Inventory::get_onhand(
326     part       => $assembly1,
327   ), "3.00000", 'get_onhand without bestbefore finds all');
328 }
329
330
331 sub reset_db {
332   SL::DB::Manager::Order->delete_all(all => 1);
333   SL::DB::Manager::Inventory->delete_all(all => 1);
334   SL::DB::Manager::Assembly->delete_all(all => 1);
335   SL::DB::Manager::Part->delete_all(all => 1);
336   SL::DB::Manager::Bin->delete_all(all => 1);
337   SL::DB::Manager::Warehouse->delete_all(all => 1);
338 }
339
340 sub create_standard_stock {
341   ($wh, $bin1)          = create_warehouse_and_bins();
342   ($wh_moon, $bin_moon) = create_warehouse_and_bins(
343       warehouse_description => 'Our warehouse location at the moon',
344       bin_description       => 'Lunar crater',
345     );
346   $bin2 = SL::DB::Bin->new(description => "Bin 2", warehouse => $wh)->save;
347   $wh->load;
348
349   $assembly1  =  new_assembly(number_of_parts => 2)->save;
350   ($part1, $part2) = map { $_->part } $assembly1->assemblies;
351 }
352
353
354 reset_db();
355
356 done_testing();
357
358 1;