1 package SL::Controller::DeliveryValueReport;
4 use parent qw(SL::Controller::Base);
9 use SL::Controller::Helper::GetModels;
10 use SL::Controller::Helper::ReportGenerator;
11 use SL::Locale::String;
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) ],
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'),
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')
53 $self->make_filter_summary;
54 $self->prepare_report;
56 my $orderitems = $self->models->get;
57 $self->calc_qtys_price($orderitems);
58 $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
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;
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);
75 my @sortable = qw(reqdate customer vendor ordnumber partnumber description);
77 # if csv report export no units (better calculation in
78 my $rp_csv_mod = ($csv_option eq 'CSV') ? 1 : '';
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) },},
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) },},
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) } },
120 $column_defs{$_}->{text} = $sort_columns{$_} for keys %column_defs;
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,
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),
154 sub make_filter_summary {
157 my $mode = 'delivery_value_report';
158 my ($business, $employee);
160 my $filter = $::form->{filter} || {};
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};
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') ],
183 part => $::locale->text('Parts'),
184 service => $::locale->text('Services'),
185 assembly => $::locale->text('Assemblies'),
187 my @flags = map { $flags{$_} } @{ $filter->{part}{type} || [] };
190 push @filter_strings, $_ if $_;
193 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
195 $self->{filter_summary} = join ', ', @filter_strings;
206 $main::lxdebug->message(0, 'vc hier:' . $vc);
207 SL::Controller::Helper::GetModels->new(
209 model => 'OrderItem',
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},
225 return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryPlan) has no vc defined";
227 sub init_all_employees {
228 return SL::DB::Manager::Employee->get_all_sorted;
230 sub init_all_businesses {
231 return SL::DB::Manager::Business->get_all_sorted;
236 my ($self, $object, %params) = @_;
238 return unless $object;
239 my $action = $params{action} || 'edit';
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;
246 return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
248 if ($object->isa('SL::DB::Part')) {
249 my $id = $object->id;
250 return "ic.pl?action=$action&id=$id";
252 if ($object->isa('SL::DB::Customer')) {
253 my $id = $object->id;
254 return "controller.pl?action=CustomerVendor/$action&id=$id&db=customer";
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
264 return unless scalar @$orderitems;
266 my %orderitems_by_id = map { $_->id => $_ } @$orderitems;
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
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 ]})
281 my $result = SL::DBUtils::selectall_hashref_query($::form, $::form->get_standard_dbh, $query, map { $_->id } @$orderitems);
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);
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);
319 SL::Controller::DeliveryValueReport - Controller for Delivery Value Report
323 Controller class for Delivery Value Report
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.
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.
345 from_table | id | to_table | id
346 orderitems | 7 | delivery_order_items | 11
352 =item C<action_list_sales_delivery_orders>