Revision 20004cac
Von G. Richardson vor mehr als 5 Jahren hinzugefügt
SL/Controller/Part.pm | ||
---|---|---|
33 | 33 |
all_buchungsgruppen all_payment_terms all_warehouses |
34 | 34 |
parts_classification_filter |
35 | 35 |
all_languages all_units all_price_factors) ], |
36 |
'scalar' => [ qw(warehouse bin) ], |
|
36 |
'scalar' => [ qw(warehouse bin stock_amounts journal) ],
|
|
37 | 37 |
); |
38 | 38 |
|
39 | 39 |
# safety |
... | ... | |
263 | 263 |
history_entries => $history_entries); |
264 | 264 |
} |
265 | 265 |
|
266 |
sub action_inventory { |
|
267 |
my ($self) = @_; |
|
268 |
|
|
269 |
$::auth->assert('warehouse_contents'); |
|
270 |
|
|
271 |
$self->stock_amounts($self->part->get_simple_stock_sql); |
|
272 |
$self->journal($self->part->get_mini_journal); |
|
273 |
|
|
274 |
$_[0]->render('part/_inventory_data', { layout => 0 }); |
|
275 |
}; |
|
276 |
|
|
266 | 277 |
sub action_update_item_totals { |
267 | 278 |
my ($self) = @_; |
268 | 279 |
|
... | ... | |
903 | 914 |
|
904 | 915 |
if ( $::form->{part}{id} ) { |
905 | 916 |
return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels customerprices prices translations partsgroup shop_parts shop_parts.shop) ]); |
917 |
} elsif ( $::form->{id} ) { |
|
918 |
return SL::DB::Part->new(id => $::form->{id})->load; # used by inventory tab |
|
906 | 919 |
} else { |
907 | 920 |
die "part_type missing" unless $::form->{part}{part_type}; |
908 | 921 |
return SL::DB::Part->new(part_type => $::form->{part}{part_type}); |
SL/DB/Part.pm | ||
---|---|---|
3 | 3 |
use strict; |
4 | 4 |
|
5 | 5 |
use Carp; |
6 |
use List::MoreUtils qw(any); |
|
6 |
use List::MoreUtils qw(any uniq);
|
|
7 | 7 |
use Rose::DB::Object::Helpers qw(as_tree); |
8 | 8 |
|
9 | 9 |
use SL::Locale::String qw(t8); |
... | ... | |
363 | 363 |
SUM(i.qty) AS qty, |
364 | 364 |
SUM(i.qty * p.lastcost) AS stock_value, |
365 | 365 |
p.unit AS unit, |
366 |
LEAD(w.description) OVER pt AS wh_lead, -- to detect warehouse changes for subtotals |
|
366 |
LEAD(w.description) OVER pt AS wh_lead, -- to detect warehouse changes for subtotals in template
|
|
367 | 367 |
SUM( SUM(i.qty) ) OVER pt AS run_qty, -- running total of total qty |
368 | 368 |
SUM( SUM(i.qty) ) OVER wh AS wh_run_qty, -- running total of warehouse qty |
369 | 369 |
SUM( SUM(i.qty * p.lastcost)) OVER pt AS run_stock_value, -- running total of total stock_value |
370 | 370 |
SUM( SUM(i.qty * p.lastcost)) OVER wh AS wh_run_stock_value -- running total of warehouse stock_value |
371 | 371 |
FROM inventory i |
372 |
LEFT JOIN parts p ON (p.id = i.parts_id) |
|
373 |
LEFT JOIN warehouse w ON (i.warehouse_id = w.id) |
|
374 |
LEFT JOIN bin b ON (i.bin_id = b.id) |
|
372 |
LEFT JOIN parts p ON (p.id = i.parts_id)
|
|
373 |
LEFT JOIN warehouse w ON (i.warehouse_id = w.id)
|
|
374 |
LEFT JOIN bin b ON (i.bin_id = b.id)
|
|
375 | 375 |
WHERE parts_id = ? |
376 | 376 |
GROUP BY w.description, b.description, p.unit, i.parts_id |
377 | 377 |
HAVING SUM(qty) != 0 |
378 |
WINDOW pt AS (PARTITION BY i.parts_id ORDER BY w.description, b.description, p.unit),
|
|
379 |
wh AS (PARTITION by w.description ORDER BY w.description, b.description, p.unit)
|
|
380 |
ORDER BY w.description, b.description
|
|
378 |
WINDOW pt AS (PARTITION BY i.parts_id ORDER BY w.sortkey, b.description, p.unit),
|
|
379 |
wh AS (PARTITION by w.description ORDER BY w.sortkey, b.description, p.unit)
|
|
380 |
ORDER BY w.sortkey, b.description, p.unit
|
|
381 | 381 |
SQL |
382 | 382 |
|
383 | 383 |
my $stock_info = selectall_hashref_query($::form, $self->db->dbh, $query, $self->id); |
384 | 384 |
return $stock_info; |
385 | 385 |
} |
386 | 386 |
|
387 |
sub get_mini_journal { |
|
388 |
my ($self) = @_; |
|
389 |
|
|
390 |
# inventory ids of the most recent 10 inventory trans_ids |
|
391 |
|
|
392 |
# duplicate code copied from SL::Controller::Inventory mini_journal, except |
|
393 |
# for the added filter on parts_id |
|
394 |
|
|
395 |
my $parts_id = $self->id; |
|
396 |
my $query = <<"SQL"; |
|
397 |
with last_inventories as ( |
|
398 |
select id, |
|
399 |
trans_id, |
|
400 |
itime |
|
401 |
from inventory |
|
402 |
where parts_id = $parts_id |
|
403 |
order by itime desc |
|
404 |
limit 20 |
|
405 |
), |
|
406 |
grouped_ids as ( |
|
407 |
select trans_id, |
|
408 |
array_agg(id) as ids |
|
409 |
from last_inventories |
|
410 |
group by trans_id |
|
411 |
order by max(itime) |
|
412 |
desc limit 10 |
|
413 |
) |
|
414 |
select unnest(ids) |
|
415 |
from grouped_ids |
|
416 |
limit 20 -- so the planner knows how many ids to expect, the cte is an optimisation fence |
|
417 |
SQL |
|
418 |
|
|
419 |
my $objs = SL::DB::Manager::Inventory->get_all( |
|
420 |
query => [ id => [ \"$query" ] ], |
|
421 |
with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template |
|
422 |
sort_by => 'itime DESC', |
|
423 |
); |
|
424 |
# remember order of trans_ids from query, for ordering hash later |
|
425 |
my @sorted_trans_ids = uniq map { $_->trans_id } @$objs; |
|
426 |
|
|
427 |
# at most 2 of them belong to a transaction and the qty determines in or out. |
|
428 |
my %transactions; |
|
429 |
for (@$objs) { |
|
430 |
$transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_; |
|
431 |
$transactions{ $_->trans_id }{base} = $_; |
|
432 |
} |
|
433 |
|
|
434 |
# because the inventory transactions were built in a hash, we need to sort the |
|
435 |
# hash by using the original sort order of the trans_ids |
|
436 |
my @sorted = map { $transactions{$_} } @sorted_trans_ids; |
|
437 |
|
|
438 |
return \@sorted; |
|
439 |
} |
|
440 |
|
|
387 | 441 |
sub clone_and_reset_deep { |
388 | 442 |
my ($self) = @_; |
389 | 443 |
|
... | ... | |
612 | 666 |
description is about to change, i.e. the next line will contain numbers from a |
613 | 667 |
different warehouse, so that a subtotal line can be added. |
614 | 668 |
|
615 |
The last line will contain the qty total and the total stock value over all |
|
616 |
warehouses/bins and can be used to add a line for the grand totals. |
|
669 |
The last row will contain the running qty total (run_qty) and the running total |
|
670 |
stock value (run_stock_value) over all warehouses/bins and can be used to add a |
|
671 |
line for the grand totals. |
|
617 | 672 |
|
618 | 673 |
=item C<items_lastcost_sum> |
619 | 674 |
|
doc/changelog | ||
---|---|---|
12 | 12 |
suchen soll. Ist dieses Feature eingeschaltet, so werden auch die Kunden- |
13 | 13 |
bzw. Lieferanten-Artikelnummern als Spalte in den Positionen angezeigt. |
14 | 14 |
|
15 |
- Part Controller - neuer Tab mit Lagerinformationen - was ist wo gelagert |
|
15 | 16 |
|
16 | 17 |
2019-08-07 - Release 3.5.4 |
17 | 18 |
|
locale/de/all | ||
---|---|---|
3016 | 3016 |
'Stock Local/Shop' => 'Bestand Lokal/Online', |
3017 | 3017 |
'Stock Qty for Date' => 'Lagerbestand am', |
3018 | 3018 |
'Stock for part #1' => 'Bestand für Artikel #1', |
3019 |
'Stock levels' => 'Lagerbestände', |
|
3019 | 3020 |
'Stock value' => 'Bestandswert', |
3020 | 3021 |
'Stocked Qty' => 'Lagermenge', |
3021 | 3022 |
'Stocktaking' => 'Inventur', |
templates/webpages/part/_inventory.html | ||
---|---|---|
1 |
[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%][%- USE T8 -%] |
|
2 |
|
|
3 |
[%- IF AUTH.assert('warehouse_management', 1) -%] |
|
4 |
<p> |
|
5 |
[% 'Actions' | $T8 %]: |
|
6 |
<span><a href="controller.pl?action=Inventory/stock_in&part_id=[% HTML.escape(SELF.part.id)%]&select_default_bin=1">[% 'Stock' | $T8 %]</a></span> |
|
7 |
<span><a href="wh.pl?trans_type=transfer&action=transfer_warehouse_selection&parts_id=[% HTML.escape(SELF.part.id) %]">[% 'Transfer' | $T8 %]</a></span> |
|
8 |
<span><a href="wh.pl?action=transfer_warehouse_selection&trans_type=removal&parts_id=[% HTML.escape(SELF.part.id) %]">[% 'Removal' | $T8 %]</a></span> |
|
9 |
</p> |
|
10 |
[%- END -%] |
|
11 |
|
|
12 |
<div id="inventory_data"> |
|
13 |
</div> |
|
14 |
|
|
15 |
<script type='text/javascript'> |
|
16 |
$(function() { |
|
17 |
$('.tabwidget').on('tabsbeforeactivate', function(event, ui){ |
|
18 |
if (ui.newPanel.attr('id') == 'inventory') { |
|
19 |
$.ajax({ |
|
20 |
url: 'controller.pl?action=Part/inventory&id=[% SELF.part.id %]', |
|
21 |
success: function (html) { |
|
22 |
$("#inventory_data").html(html); |
|
23 |
}, |
|
24 |
}); |
|
25 |
} |
|
26 |
return 1; |
|
27 |
}); |
|
28 |
}); |
|
29 |
</script> |
templates/webpages/part/_inventory_data.html | ||
---|---|---|
1 |
[%- USE HTML %][%- USE L -%][%- USE P -%][%- USE LxERP -%][%- USE T8 -%] |
|
2 |
|
|
3 |
[%- SET dec = 2 %] |
|
4 |
[%- SET show_warehouse_subtotals = 1 %] |
|
5 |
|
|
6 |
<div id="stock_levels"> |
|
7 |
|
|
8 |
<h3>[% 'Stock levels' | $T8 %]</h3> |
|
9 |
|
|
10 |
[%- IF SELF.stock_amounts.size %] |
|
11 |
<a href="wh.pl?action=report&partnumber=[% HTML.escape(SELF.part.partnumber) %]">[% 'Stock levels' | $T8 %]</a>: |
|
12 |
<table> |
|
13 |
<thead> |
|
14 |
<tr class='listheading'> |
|
15 |
<th>[% 'Warehouse' | $T8 %]</th> |
|
16 |
<th>[% 'Bin' | $T8 %]</th> |
|
17 |
<th>[% 'Qty' | $T8 %]</th> |
|
18 |
<th>[% 'Unit' | $T8 %]</th> |
|
19 |
<th>[% 'Stock value' | $T8 %]</th> |
|
20 |
</tr> |
|
21 |
</thead> |
|
22 |
<tbody> |
|
23 |
[% FOREACH stock = SELF.stock_amounts %] |
|
24 |
<tr class='listrow'> |
|
25 |
<td >[% HTML.escape(stock.warehouse_description) %]</td> |
|
26 |
<td >[% IF stock.order_link %]<a target="_blank" href="[% stock.order_link %]">[% END %] |
|
27 |
[% HTML.escape(stock.bin_description) %] |
|
28 |
[% IF stock.order_link %]</a>[% END %] |
|
29 |
</td> |
|
30 |
<td class='numeric'>[% LxERP.format_amount(stock.qty, dec) %]</td> |
|
31 |
<td >[% HTML.escape(stock.unit) %]</td> |
|
32 |
<td class='numeric'>[% LxERP.format_amount(stock.stock_value, 2) %]</td> |
|
33 |
</tr> |
|
34 |
[% IF show_warehouse_subtotals AND stock.wh_lead != stock.warehouse_description %] |
|
35 |
<tr class='listheading'> |
|
36 |
<th >[% HTML.escape(stock.warehouse_description) %]</th> |
|
37 |
<td></td> |
|
38 |
<td class='numeric bold'>[% LxERP.format_amount(stock.wh_run_qty, dec) %]</td> |
|
39 |
<td></td> |
|
40 |
<td class='numeric bold'>[% LxERP.format_amount(stock.wh_run_stock_value, dec) %]</td> |
|
41 |
</tr> |
|
42 |
[% END %] |
|
43 |
[% IF loop.last %] |
|
44 |
<tr class='listheading'> |
|
45 |
<th>[% 'Total' | $T8 %]</th> |
|
46 |
<td></td> |
|
47 |
<td class='numeric bold'>[% LxERP.format_amount(stock.run_qty, dec) %]</td> |
|
48 |
<td></td> |
|
49 |
<td class='numeric bold'>[% LxERP.format_amount(stock.run_stock_value, dec) %]</td> |
|
50 |
</tr> |
|
51 |
[% END %] |
|
52 |
[% END %] |
|
53 |
</tbody> |
|
54 |
</table> |
|
55 |
[% ELSE %] |
|
56 |
<p>[% 'No transactions yet.' | $T8 %]</p> |
|
57 |
[% END %] |
|
58 |
</div> |
|
59 |
|
|
60 |
[% IF AUTH.assert('warehouse_management', 1) %] |
|
61 |
<div> |
|
62 |
<h3>[% 'Journal of Last 10 Transfers' | $T8 %]</h3> |
|
63 |
<a href="wh.pl?action=journal&partnumber=[% HTML.escape(SELF.part.partnumber) %]">[% 'WHJournal' | $T8 %]</a>: |
|
64 |
[%- IF SELF.journal.size %] |
|
65 |
<table> |
|
66 |
<tr class='listheading'> |
|
67 |
<th>[% 'Date' | $T8 %]</th> |
|
68 |
<th>[% 'Trans Type' | $T8 %]</th> |
|
69 |
<th>[% 'Warehouse From' | $T8 %]</th> |
|
70 |
<th>[% 'Qty' | $T8 %]</th> |
|
71 |
<th>[% 'Unit' | $T8 %]</th> |
|
72 |
<th>[% 'Warehouse To' | $T8 %]</th> |
|
73 |
<th>[% 'Charge Number' | $T8 %]</th> |
|
74 |
<th>[% 'Comment' | $T8 %]</th> |
|
75 |
</tr> |
|
76 |
[% FOREACH row = SELF.journal %] |
|
77 |
<tr class='listrow'> |
|
78 |
<td>[% row.base.itime_as_date %]</td> |
|
79 |
<td>[% row.base.trans_type.description | $T8 %]</td> |
|
80 |
<td>[% row.out ? row.out.bin.full_description : '-' | html %]</td> |
|
81 |
<td class='numeric'>[% row.in ? row.in.qty_as_number : LxERP.format_amount(-1 * row.out.qty, 2) %]</td> |
|
82 |
<td>[% row.base.part.unit | html %]</td> |
|
83 |
<td>[% row.in ? row.in.bin.full_description : '-' | html %]</td> |
|
84 |
<td>[% row.base.chargenumber | html %]</td> |
|
85 |
<td>[% row.base.comment | html %]</td> |
|
86 |
</tr> |
|
87 |
[% END %] |
|
88 |
</table> |
|
89 |
[%- ELSE %] |
|
90 |
<p>[% 'No transactions yet.' | $T8 %]</p> |
|
91 |
[%- END %] |
|
92 |
</div> |
|
93 |
[% END # assert warehouse_management %] |
templates/webpages/part/form.html | ||
---|---|---|
38 | 38 |
[%- IF SELF.part.id %] |
39 | 39 |
<li><a href="#price_rules">[% 'Price Rules' | $T8 %]</a></li> |
40 | 40 |
[% END %] |
41 |
[%- IF (AUTH.assert('warehouse_contents', 1) AND SELF.part.id AND NOT SELF.part.is_service) %] |
|
42 |
<li><a href="#inventory">[% 'Inventories' | $T8 %]</a></li> |
|
43 |
[%- END %] |
|
41 | 44 |
[%- IF CUSTOM_VARIABLES.size %] |
42 | 45 |
<li><a href="#custom_variables">[% 'Custom Variables' | $T8 %]</a></li> |
43 | 46 |
[%- END %] |
... | ... | |
75 | 78 |
[% PROCESS 'part/_shop.html' %] |
76 | 79 |
</div> |
77 | 80 |
[%- END %] |
81 |
|
|
82 |
[%- IF AUTH.assert('warehouse_contents', 1) AND SELF.part.id AND NOT SELF.part.is_service %] |
|
83 |
<div id="inventory"> |
|
84 |
[% PROCESS 'part/_inventory.html' %] |
|
85 |
</div> |
|
86 |
[%- END %] |
|
87 |
|
|
78 | 88 |
[%- END %] |
79 | 89 |
|
80 | 90 |
[%- IF CUSTOM_VARIABLES.size %] |
Auch abrufbar als: Unified diff
Part Controller - neuer Tab mit Lagerinformationen
(Derzeit gibt es im Template Variabeln um Zwischensummen und
Nachkommastellen zu kontrollieren)
Diese Daten werden nur bei Bedarf geladen, also wenn der Benutzer auf
den neuen Tab "Lagerbewegungen/-bestände" klickt.
Außerdem gibt es Links zu diversen Lageraktionen (Einlagern, Umlagern,
Entnahme), wo der Artikel dann schon vorausgewählt ist.