|
1 |
package SL::Controller::DeliveryValueReport;
|
|
2 |
|
|
3 |
use strict;
|
|
4 |
use parent qw(SL::Controller::Base);
|
|
5 |
|
|
6 |
use Clone qw(clone);
|
|
7 |
use SL::DB::OrderItem;
|
|
8 |
use SL::DB::Business;
|
|
9 |
use SL::Controller::Helper::GetModels;
|
|
10 |
use SL::Controller::Helper::ReportGenerator;
|
|
11 |
use SL::Locale::String;
|
|
12 |
use SL::AM;
|
|
13 |
use SL::DBUtils ();
|
|
14 |
use Carp;
|
|
15 |
use Data::Dumper;
|
|
16 |
|
|
17 |
use Rose::Object::MakeMethods::Generic (
|
|
18 |
scalar => [ qw(db_args flat_filter) ],
|
|
19 |
'scalar --get_set_init' => [ qw(models vc all_employees all_businesses) ],
|
|
20 |
);
|
|
21 |
|
|
22 |
|
|
23 |
|
|
24 |
my %sort_columns = (
|
|
25 |
reqdate => t8('Reqdate'),
|
|
26 |
customer => t8('Customer'),
|
|
27 |
vendor => t8('Vendor'),
|
|
28 |
ordnumber => t8('Order'),
|
|
29 |
partnumber => t8('Part Number'),
|
|
30 |
description => t8('Description'),
|
|
31 |
qty => t8('Qty in Order'),
|
|
32 |
unit => t8('Unit'),
|
|
33 |
netto_qty => t8('Net value in Order'),
|
|
34 |
not_shipped_qty => t8('not shipped'),
|
|
35 |
netto_not_shipped_qty => t8('Net value without delivery orders'),
|
|
36 |
shipped_qty => t8('Qty in delivery orders'),
|
|
37 |
netto_shipped_qty => t8('Net Value in delivery orders'),
|
|
38 |
delivered_qty => t8('transferred in / out'),
|
|
39 |
netto_delivered_qty => t8('Net value transferred in / out'),
|
|
40 |
do_closed_qty => t8('Qty in closed delivery orders'),
|
|
41 |
netto_do_closed_qty => t8('Qty in closed delivery orders')
|
|
42 |
);
|
|
43 |
|
|
44 |
|
|
45 |
|
|
46 |
|
|
47 |
#
|
|
48 |
# action
|
|
49 |
#
|
|
50 |
|
|
51 |
sub action_list {
|
|
52 |
my ($self) = @_;
|
|
53 |
$self->make_filter_summary;
|
|
54 |
$self->prepare_report;
|
|
55 |
|
|
56 |
my $orderitems = $self->models->get;
|
|
57 |
$self->calc_qtys_price($orderitems);
|
|
58 |
$self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
|
|
59 |
}
|
|
60 |
|
|
61 |
sub prepare_report {
|
|
62 |
my ($self) = @_;
|
|
63 |
|
|
64 |
my $vc = $self->vc;
|
|
65 |
my $mode = 'delivery_value_report';
|
|
66 |
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
|
|
67 |
my $csv_option = $::form->{report_generator_output_format};
|
|
68 |
$self->{report} = $report;
|
|
69 |
|
|
70 |
my @columns = qw(reqdate customer vendor ordnumber partnumber description unit qty netto_qty
|
|
71 |
not_shipped_qty netto_not_shipped_qty shipped_qty netto_shipped_qty delivered_qty
|
|
72 |
netto_delivered_qty do_closed_qty netto_do_closed_qty);
|
|
73 |
|
|
74 |
|
|
75 |
my @sortable = qw(reqdate customer vendor ordnumber partnumber description);
|
|
76 |
|
|
77 |
# if csv report export no units (better calculation in
|
|
78 |
my $rp_csv_mod = ($csv_option eq 'CSV') ? 1 : '';
|
|
79 |
|
|
80 |
my %column_defs = (
|
|
81 |
reqdate => { sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date } },
|
|
82 |
description => { sub => sub { $_[0]->description },
|
|
83 |
obj_link => sub { $self->link_to($_[0]->part) } },
|
|
84 |
partnumber => { sub => sub { $_[0]->part->partnumber },
|
|
85 |
obj_link => sub { $self->link_to($_[0]->part) } },
|
|
86 |
qty => { sub => sub { $_[0]->qty_as_number .
|
|
87 |
($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
|
|
88 |
netto_qty => { sub => sub { $::form->format_amount(\%::myconfig,
|
|
89 |
($_[0]->qty * $_[0]->sellprice * (1 - $_[0]->discount) /
|
|
90 |
($_[0]->price_factor || 1), 2)) },},
|
|
91 |
unit => { sub => sub { $_[0]->unit },
|
|
92 |
visible => $rp_csv_mod },
|
|
93 |
shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{shipped_qty}, 2) .
|
|
94 |
($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
|
|
95 |
netto_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_shipped_qty}, 2) },},
|
|
96 |
not_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty}, 2) .
|
|
97 |
($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
|
|
98 |
delivered_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{delivered_qty}, 2) .
|
|
99 |
($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
|
|
100 |
netto_delivered_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_delivered_qty}, 2) },},
|
|
101 |
|
|
102 |
netto_not_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig,(
|
|
103 |
($_[0]->qty - $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty})
|
|
104 |
* ($_[0]->sellprice * (1 - $_[0]->discount) / ($_[0]->price_factor || 1)), 2)) },},
|
|
105 |
do_closed_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{do_closed_qty}, 2) .
|
|
106 |
($rp_csv_mod ? '' : ' ' . $_[0]->unit) },},
|
|
107 |
|
|
108 |
netto_do_closed_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_do_closed_qty}, 2) },
|
|
109 |
visible => $mode eq 'delivery_value_report' },
|
|
110 |
ordnumber => { sub => sub { $_[0]->order->ordnumber },
|
|
111 |
obj_link => sub { $self->link_to($_[0]->order) } },
|
|
112 |
vendor => { sub => sub { $_[0]->order->vendor->name },
|
|
113 |
visible => $vc eq 'vendor',
|
|
114 |
obj_link => sub { $self->link_to($_[0]->order->vendor) } },
|
|
115 |
customer => { sub => sub { $_[0]->order->customer->name },
|
|
116 |
visible => $vc eq 'customer',
|
|
117 |
obj_link => sub { $self->link_to($_[0]->order->customer) } },
|
|
118 |
);
|
|
119 |
|
|
120 |
$column_defs{$_}->{text} = $sort_columns{$_} for keys %column_defs;
|
|
121 |
|
|
122 |
$report->set_options(
|
|
123 |
std_column_visibility => 1,
|
|
124 |
controller_class => 'DeliveryValueReport',
|
|
125 |
output_format => 'HTML',
|
|
126 |
top_info_text => ($vc eq 'customer') ? (($mode eq 'delivery_plan') ? $::locale->text('Delivery Plan for currently outstanding sales orders') :
|
|
127 |
$::locale->text('Delivery Value Report for currently open sales orders')) :
|
|
128 |
($mode eq 'delivery_value_report') ? $::locale->text('Delivery Value Report for currently outstanding purchase orders') :
|
|
129 |
$::locale->text('Delivery Plan for currently outstanding purchase orders'),
|
|
130 |
title => ($mode eq 'delivery_plan') ? $::locale->text('Delivery Plan') : $::locale->text('Delivery Value Report'),
|
|
131 |
allow_pdf_export => 1,
|
|
132 |
allow_csv_export => 1,
|
|
133 |
);
|
|
134 |
$report->set_columns(%column_defs);
|
|
135 |
$report->set_column_order(@columns);
|
|
136 |
$report->set_export_options(qw(list filter vc));
|
|
137 |
$report->set_options_from_form;
|
|
138 |
$self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
|
|
139 |
$self->models->finalize; # for filter laundering
|
|
140 |
$self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
|
|
141 |
$report->set_options(
|
|
142 |
raw_top_info_text => $self->render('delivery_value_report/report_top', { output => 0 }),
|
|
143 |
raw_bottom_info_text => $self->render('delivery_value_report/report_bottom', { output => 0 }, models => $self->models),
|
|
144 |
);
|
|
145 |
}
|
|
146 |
|
|
147 |
|
|
148 |
|
|
149 |
|
|
150 |
#
|
|
151 |
# filter
|
|
152 |
#
|
|
153 |
|
|
154 |
sub make_filter_summary {
|
|
155 |
my ($self) = @_;
|
|
156 |
my $vc = $self->vc;
|
|
157 |
my $mode = 'delivery_value_report';
|
|
158 |
my ($business, $employee);
|
|
159 |
|
|
160 |
my $filter = $::form->{filter} || {};
|
|
161 |
my @filter_strings;
|
|
162 |
|
|
163 |
$business = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
|
|
164 |
$employee = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name if $filter->{order}{employee_id};
|
|
165 |
|
|
166 |
my @filters = (
|
|
167 |
[ $filter->{order}{"ordnumber:substr::ilike"}, $::locale->text('Number') ],
|
|
168 |
[ $filter->{order}{globalproject}{"projectnumber:substr::ilike"}, $::locale->text('Document Project Number') ],
|
|
169 |
[ $filter->{part}{"partnumber:substr::ilike"}, $::locale->text('Part Number') ],
|
|
170 |
[ $filter->{"description:substr::ilike"}, $::locale->text('Part Description') ],
|
|
171 |
[ $filter->{"reqdate:date::ge"}, $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
|
|
172 |
[ $filter->{"reqdate:date::le"}, $::locale->text('Delivery Date') . " " . $::locale->text('To Date') ],
|
|
173 |
[ $filter->{"qty:number"}, $::locale->text('Quantity') ],
|
|
174 |
[ $filter->{order}{vendor}{"name:substr::ilike"}, $::locale->text('Vendor') ],
|
|
175 |
[ $filter->{order}{vendor}{"vendornumber:substr::ilike"}, $::locale->text('Vendor Number') ],
|
|
176 |
[ $filter->{order}{customer}{"name:substr::ilike"}, $::locale->text('Customer') ],
|
|
177 |
[ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number') ],
|
|
178 |
[ $business, $::locale->text('Customer type') ],
|
|
179 |
[ $employee, $::locale->text('Employee') ],
|
|
180 |
);
|
|
181 |
|
|
182 |
my %flags = (
|
|
183 |
part => $::locale->text('Parts'),
|
|
184 |
service => $::locale->text('Services'),
|
|
185 |
assembly => $::locale->text('Assemblies'),
|
|
186 |
);
|
|
187 |
my @flags = map { $flags{$_} } @{ $filter->{part}{type} || [] };
|
|
188 |
|
|
189 |
for (@flags) {
|
|
190 |
push @filter_strings, $_ if $_;
|
|
191 |
}
|
|
192 |
for (@filters) {
|
|
193 |
push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
|
|
194 |
}
|
|
195 |
$self->{filter_summary} = join ', ', @filter_strings;
|
|
196 |
}
|
|
197 |
|
|
198 |
|
|
199 |
|
|
200 |
#
|
|
201 |
# helpers
|
|
202 |
#
|
|
203 |
sub init_models {
|
|
204 |
my ($self) = @_;
|
|
205 |
my $vc = $self->vc;
|
|
206 |
$main::lxdebug->message(0, 'vc hier:' . $vc);
|
|
207 |
SL::Controller::Helper::GetModels->new(
|
|
208 |
controller => $self,
|
|
209 |
model => 'OrderItem',
|
|
210 |
sorted => {
|
|
211 |
_default => {
|
|
212 |
by => 'reqdate',
|
|
213 |
dir => 1,
|
|
214 |
},
|
|
215 |
%sort_columns,
|
|
216 |
},
|
|
217 |
# show only open orders
|
|
218 |
query => [ 'order.closed' => '0', "order.${vc}_id" => { gt => 0 } ],
|
|
219 |
with_objects => [ 'order', "order.$vc" ],
|
|
220 |
additional_url_params => { vc => $vc},
|
|
221 |
);
|
|
222 |
}
|
|
223 |
|
|
224 |
sub init_vc {
|
|
225 |
return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryPlan) has no vc defined";
|
|
226 |
}
|
|
227 |
sub init_all_employees {
|
|
228 |
return SL::DB::Manager::Employee->get_all_sorted;
|
|
229 |
}
|
|
230 |
sub init_all_businesses {
|
|
231 |
return SL::DB::Manager::Business->get_all_sorted;
|
|
232 |
}
|
|
233 |
|
|
234 |
|
|
235 |
sub link_to {
|
|
236 |
my ($self, $object, %params) = @_;
|
|
237 |
|
|
238 |
return unless $object;
|
|
239 |
my $action = $params{action} || 'edit';
|
|
240 |
|
|
241 |
if ($object->isa('SL::DB::Order')) {
|
|
242 |
my $type = $object->type;
|
|
243 |
my $vc = $object->is_sales ? 'customer' : 'vendor';
|
|
244 |
my $id = $object->id;
|
|
245 |
|
|
246 |
return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
|
|
247 |
}
|
|
248 |
if ($object->isa('SL::DB::Part')) {
|
|
249 |
my $id = $object->id;
|
|
250 |
return "ic.pl?action=$action&id=$id";
|
|
251 |
}
|
|
252 |
if ($object->isa('SL::DB::Customer')) {
|
|
253 |
my $id = $object->id;
|
|
254 |
return "controller.pl?action=CustomerVendor/$action&id=$id&db=customer";
|
|
255 |
}
|
|
256 |
}
|
|
257 |
|
|
258 |
|
|
259 |
sub calc_qtys_price {
|
|
260 |
my ($self, $orderitems) = @_;
|
|
261 |
# using $orderitem->shipped_qty 40 times is far too slow. need to do it manually
|
|
262 |
#
|
|
263 |
|
|
264 |
return unless scalar @$orderitems;
|
|
265 |
|
|
266 |
my %orderitems_by_id = map { $_->id => $_ } @$orderitems;
|
|
267 |
|
|
268 |
my $query = <<SQL;
|
|
269 |
SELECT oi.id, doi.qty, doi.qty, doi.unit, doe.delivered, doe.closed, -- dois.qty as qty_stocked,
|
|
270 |
oi.sellprice, oi.discount, oi.price_factor
|
|
271 |
FROM record_links rl
|
|
272 |
INNER JOIN delivery_order_items doi ON (doi.id = rl.to_id)
|
|
273 |
INNER JOIN orderitems oi ON (oi.id = rl.from_id)
|
|
274 |
INNER JOIN delivery_orders doe ON (doe.id = doi.delivery_order_id)
|
|
275 |
--INNER JOIN delivery_order_items_stock dois ON (doi.id = dois.delivery_order_item_id)
|
|
276 |
WHERE rl.from_table = 'orderitems'
|
|
277 |
AND rl.to_table = 'delivery_order_items'
|
|
278 |
AND oi.id IN (@{[ join ', ', ("?")x @$orderitems ]})
|
|
279 |
SQL
|
|
280 |
|
|
281 |
my $result = SL::DBUtils::selectall_hashref_query($::form, $::form->get_standard_dbh, $query, map { $_->id } @$orderitems);
|
|
282 |
|
|
283 |
for my $row (@$result) {
|
|
284 |
my $item = $orderitems_by_id{ $row->{id} };
|
|
285 |
$item->{shipped_qty} ||= 0;
|
|
286 |
$item->{delivered_qty} ||= 0;
|
|
287 |
$item->{do_closed_qty} ||= 0;
|
|
288 |
$item->{shipped_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} unless ($row->{delivered} || $row->{closed});
|
|
289 |
$item->{delivered_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} if ($row->{delivered} && !$row->{closed});
|
|
290 |
$item->{do_closed_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} if ($row->{closed});
|
|
291 |
#$item->{do_closed_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty_stocked} if ($row->{closed});
|
|
292 |
$item->{not_shipped_qty} += AM->convert_unit($row->{unit} => $item->unit) * $row->{qty} unless ($row->{delivered});
|
|
293 |
# my $price_factor = $self->price_factor || 1;
|
|
294 |
#$self->_delivered_qty;
|
|
295 |
#$item->{netto_qty} += $row->{qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
|
|
296 |
# no sum
|
|
297 |
$item->{netto_shipped_qty} = $item->{shipped_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
|
|
298 |
# $item->{netto_not_shipped_qty} += $item->{shipped_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
|
|
299 |
$item->{netto_delivered_qty} = $item->{delivered_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
|
|
300 |
$item->{netto_do_closed_qty} = $item->{do_closed_qty} * $row->{sellprice} * (1 - $row->{discount} ) / ($row->{price_factor} || 1);
|
|
301 |
|
|
302 |
}
|
|
303 |
}
|
|
304 |
|
|
305 |
|
|
306 |
|
|
307 |
|
|
308 |
1;
|
|
309 |
|
|
310 |
|
|
311 |
__END__
|
|
312 |
|
|
313 |
=pod
|
|
314 |
|
|
315 |
=encoding utf8
|
|
316 |
|
|
317 |
=head1 NAME
|
|
318 |
|
|
319 |
SL::Controller::DeliveryValueReport - Controller for Delivery Value Report
|
|
320 |
|
|
321 |
=head2 OVERVIEW
|
|
322 |
|
|
323 |
Controller class for Delivery Value Report
|
|
324 |
|
|
325 |
The goal of the record is to determine which goods and at what costs are already delivered, transfered in
|
|
326 |
relation to open orders, orders in process.
|
|
327 |
|
|
328 |
|
|
329 |
Inherited from the base controller class, this controller implements the Delivery Value Report.
|
|
330 |
Historically the idea derived from a customer extension by thinking: Ah, we just need the Delivery Plan
|
|
331 |
put some more columns in it and then we have a pseudo "Production, Planing, Report" with an additional
|
|
332 |
emphasis on expected future cashflow.
|
|
333 |
Some problems exists with the current report: The definition of not fully delivered sales / purchase order
|
|
334 |
is very (customer) special, in general a simple check on order is open should be a sensible workflow value.
|
|
335 |
Secondly a major database flaw (no persistent ids in order_items) made it impossible to determine the origin
|
|
336 |
of items in terms of linked records. One assumption build in the original DeliveryPlan was that the part_ids
|
|
337 |
are equal. This breaks if the document has the same item on different positions. The next idea was to check
|
|
338 |
for individual item reqdates.
|
|
339 |
After some arguing we decided to implement persistent ids for all items and link them directly via record_links.
|
|
340 |
This linking has been secrectly active since version 3.2, therefore this redesign is possible.
|
|
341 |
Currently the report even works correctly even if the same part has been manually put in another position, renamed or some
|
|
342 |
other metadata for the position has been altered. This is due to the fact that a hidden converted_from_previous_document is
|
|
343 |
used in the position.
|
|
344 |
|
|
345 |
from_table | id | to_table | id
|
|
346 |
orderitems | 7 | delivery_order_items | 11
|
|
347 |
|
|
348 |
=head1 FUNCTIONS
|
|
349 |
|
|
350 |
=over 2
|
|
351 |
|
|
352 |
=item C<action_list_sales_delivery_orders>
|
|
353 |
|
|
354 |
L
|
Lieferwertbericht auf eigenen Controller umgestellt und erweitert
Erweiterungen aus einem Kundenprojekt übernommen. Ferner performanter
gemacht, in Anlehnung an calc_qts aus DeliveryPlan. Zusätzlich
das model einfacher umgesetzt und die Auswertung basiert jetzt auf
der Verknüpfung von orderitems(id) -> delivery_order_items(id) und
nicht mehr über die Näherung über die verknüpften Belege.