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;
12 use SL::Helper::ShippedQty;
18 use Rose::Object::MakeMethods::Generic (
19 scalar => [ qw(db_args flat_filter) ],
20 'scalar --get_set_init' => [ qw(models vc all_employees all_businesses) ],
23 __PACKAGE__->run_before(sub { $::auth->assert('delivery_value_report'); });
26 reqdate => t8('Reqdate'),
27 customer => t8('Customer'),
28 vendor => t8('Vendor'),
29 ordnumber => t8('Order'),
30 partnumber => t8('Part Number'),
31 description => t8('Description'),
32 qty => t8('Qty in Order'),
34 netto_qty => t8('Net value in Order'),
35 not_shipped_qty => t8('not shipped'),
36 netto_not_shipped_qty => t8('Net value without delivery orders'),
37 shipped_qty => t8('Qty in delivery orders'),
38 netto_shipped_qty => t8('Net Value in delivery orders'),
39 delivered_qty => t8('transferred in / out'),
40 netto_delivered_qty => t8('Net value transferred in / out'),
52 $self->make_filter_summary;
53 $self->prepare_report;
55 my $orderitems = $self->models->get;
56 $self->calc_qtys_price($orderitems);
57 $self->setup_list_action_bar;
58 $self->report_generator_list_objects(report => $self->{report}, objects => $orderitems);
65 my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
66 my $csv_option = $::form->{report_generator_output_format};
67 $self->{report} = $report;
69 my @columns = qw(reqdate customer vendor ordnumber partnumber description unit qty netto_qty
70 not_shipped_qty netto_not_shipped_qty shipped_qty netto_shipped_qty delivered_qty
74 my @sortable = qw(reqdate customer vendor ordnumber partnumber description);
76 # if csv report export no units
77 my $rp_csv_mod = ($csv_option eq 'CSV') ? 1 : '';
80 reqdate => { sub => sub { $_[0]->reqdate_as_date || $_[0]->order->reqdate_as_date } },
81 description => { sub => sub { $_[0]->description },
82 obj_link => sub { $self->link_to($_[0]->part) } },
83 partnumber => { sub => sub { $_[0]->part->partnumber },
84 obj_link => sub { $self->link_to($_[0]->part) } },
85 qty => { sub => sub { $_[0]->qty_as_number .
86 ($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
87 netto_qty => { sub => sub { $::form->format_amount(\%::myconfig,
88 ($_[0]->qty * $_[0]->sellprice * (1 - $_[0]->discount) /
89 ($_[0]->price_factor || 1), 2)) },},
90 unit => { sub => sub { $_[0]->unit },
91 visible => $rp_csv_mod },
92 shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{shipped_qty}, 2) .
93 ($rp_csv_mod ? '' : ' ' . $_[0]->unit) } },
94 netto_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]{netto_shipped_qty}, 2) },},
95 not_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig, $_[0]->qty - $_[0]{shipped_qty}
96 - $_[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 netto_not_shipped_qty => { sub => sub { $::form->format_amount(\%::myconfig,(($_[0]->qty -
102 $_[0]{shipped_qty} - $_[0]{delivered_qty} - $_[0]{do_closed_qty})
103 * ($_[0]->sellprice * (1 - $_[0]->discount) /
104 ($_[0]->price_factor || 1)), 2)) },},
105 ordnumber => { sub => sub { $_[0]->order->ordnumber },
106 obj_link => sub { $self->link_to($_[0]->order) } },
107 vendor => { sub => sub { $_[0]->order->vendor->name },
108 visible => $vc eq 'vendor',
109 obj_link => sub { $self->link_to($_[0]->order->vendor) } },
110 customer => { sub => sub { $_[0]->order->customer->name },
111 visible => $vc eq 'customer',
112 obj_link => sub { $self->link_to($_[0]->order->customer) } },
115 $column_defs{$_}->{text} = $sort_columns{$_} for keys %column_defs;
117 $report->set_options(
118 std_column_visibility => 1,
119 controller_class => 'DeliveryValueReport',
120 output_format => 'HTML',
121 top_info_text => ($vc eq 'customer') ? t8('Delivery Value Report for currently open sales orders') :
122 t8('Delivery Value Report for currently outstanding purchase orders'),
123 title => $::locale->text('Delivery Value Report'),
124 allow_pdf_export => 1,
125 allow_csv_export => 1,
127 $report->set_columns(%column_defs);
128 $report->set_column_order(@columns);
129 $report->set_export_options(qw(list filter vc));
130 $report->set_options_from_form;
131 $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
132 $self->models->finalize; # for filter laundering
133 $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
134 $report->set_options(
135 raw_top_info_text => $self->render('delivery_value_report/report_top', { output => 0 }),
136 raw_bottom_info_text => $self->render('delivery_value_report/report_bottom', { output => 0 }, models => $self->models),
147 sub make_filter_summary {
150 my ($business, $employee);
152 my $filter = $::form->{filter} || {};
155 $business = SL::DB::Business->new(id => $filter->{order}{customer}{"business_id"})->load->description if $filter->{order}{customer}{"business_id"};
156 $employee = SL::DB::Employee->new(id => $filter->{order}{employee_id})->load->name if $filter->{order}{employee_id};
159 [ $filter->{order}{"ordnumber:substr::ilike"}, $::locale->text('Number') ],
160 [ $filter->{order}{globalproject}{"projectnumber:substr::ilike"}, $::locale->text('Document Project Number') ],
161 [ $filter->{part}{"partnumber:substr::ilike"}, $::locale->text('Part Number') ],
162 [ $filter->{"description:substr::ilike"}, $::locale->text('Part Description') ],
163 [ $filter->{"reqdate:date::ge"}, $::locale->text('Delivery Date') . " " . $::locale->text('From Date') ],
164 [ $filter->{"reqdate:date::le"}, $::locale->text('Delivery Date') . " " . $::locale->text('To Date') ],
165 [ $filter->{"qty:number"}, $::locale->text('Quantity') ],
166 [ $filter->{order}{vendor}{"name:substr::ilike"}, $::locale->text('Vendor') ],
167 [ $filter->{order}{vendor}{"vendornumber:substr::ilike"}, $::locale->text('Vendor Number') ],
168 [ $filter->{order}{customer}{"name:substr::ilike"}, $::locale->text('Customer') ],
169 [ $filter->{order}{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number') ],
170 [ $business, $::locale->text('Customer type') ],
171 [ $employee, $::locale->text('Employee') ],
174 # flags for with_object 'part'
176 part => $::locale->text('Parts'),
177 service => $::locale->text('Services'),
178 assembly => $::locale->text('Assemblies'),
180 my @flags = map { $flags{$_} } @{ $filter->{part}{type} || [] };
183 push @filter_strings, $_ if $_;
186 push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
188 $self->{filter_summary} = join ', ', @filter_strings;
199 SL::Controller::Helper::GetModels->new(
201 model => 'OrderItem',
209 # show only open (sales|purchase) orders
210 query => [ 'order.closed' => '0', "order.${vc}_id" => { gt => 0 },
211 'order.quotation' => 0 ],
212 with_objects => [ 'order', "order.$vc", 'part' ],
213 additional_url_params => { vc => $vc},
218 return $::form->{vc} if ($::form->{vc} eq 'customer' || $::form->{vc} eq 'vendor') || croak "self (DeliveryValueReport) has no vc defined";
220 sub init_all_employees {
221 return SL::DB::Manager::Employee->get_all_sorted;
223 sub init_all_businesses {
224 return SL::DB::Manager::Business->get_all_sorted;
229 my ($self, $object, %params) = @_;
231 return unless $object;
232 my $action = $params{action} || 'edit';
234 if ($object->isa('SL::DB::Order')) {
235 my $type = $object->type;
236 my $vc = $object->is_sales ? 'customer' : 'vendor';
237 my $id = $object->id;
239 return "oe.pl?action=$action&type=$type&vc=$vc&id=$id";
241 if ($object->isa('SL::DB::Part')) {
242 my $id = $object->id;
243 return "controller.pl?action=Part/$action&part.id=$id";
245 if ($object->isa('SL::DB::Customer')) {
246 my $id = $object->id;
247 return "controller.pl?action=CustomerVendor/$action&id=$id&db=customer";
252 sub calc_qtys_price {
253 my ($self, $orderitems) = @_;
255 return unless scalar @$orderitems;
257 SL::Helper::ShippedQty
258 ->new(require_stock_out => 1)
259 ->calculate($orderitems)
262 $_->{delivered_qty} = delete $_->{shipped_qty} for @$orderitems;
264 SL::Helper::ShippedQty
265 ->new(require_stock_out => 0)
266 ->calculate($orderitems)
269 for my $item (@$orderitems) {
270 $item->{not_shipped_qty} = $item->qty - $item->{shipped_qty};
272 my $price_factor = $item->price_factor || 1;
273 $item->{netto_shipped_qty} = $item->{shipped_qty} * $item->sellprice * (1 - $item->discount) / $price_factor;
274 $item->{netto_delivered_qty} = $item->{delivered_qty} * $item->sellprice * (1 - $item->discount) / $price_factor;
278 sub setup_list_action_bar {
279 my ($self, %params) = @_;
281 for my $bar ($::request->layout->get('actionbar')) {
285 submit => [ '#filter_form', { action => 'DeliveryValueReport/list' } ],
286 accesskey => 'enter',
303 SL::Controller::DeliveryValueReport - Controller for Delivery Value Report
307 Controller class for Delivery Value Report
309 The goal of the report is to determine which goods and at what costs are already delivered, transfered in
310 relation to open orders, orders in process.
313 Inherited from the base controller class, this controller implements the Delivery Value Report.
314 Historically the idea derived from a customer extension by thinking: Ah, we just need the Delivery Plan
315 put some more columns in it and then we have a pseudo "Production, Planing, Report" with an additional
316 emphasis on expected future cashflow.
317 Some problems exists with the current report: The definition of not fully delivered sales / purchase order
318 is very (customer) special, in general a simple check on order is open should be a sensible workflow value.
319 Secondly a major database flaw (no persistent ids in order_items) made it impossible to determine the origin
320 of items in terms of linked records. One assumption build in the original DeliveryPlan was that the part_ids
321 are equal. This breaks if the document has the same item on different positions. The next idea was to check
322 for individual item reqdates.
323 After some arguing we decided to implement persistent ids for all items and link them directly via record_links.
324 This linking has been secrectly active since version 3.2, therefore this redesign is possible.
325 Currently the report even works correctly even if the same part has been manually put in another position, renamed or some
326 other metadata for the position has been altered. This is due to the fact that a hidden converted_from_previous_document is
327 used in the position.
329 The main intelligence is this query (qty_stocked as comments):
331 SELECT oi.id,and more metadata , -- dois.qty as qty_stocked,
333 INNER JOIN delivery_order_items doi ON (doi.id = rl.to_id)
334 INNER JOIN orderitems oi ON (oi.id = rl.from_id)
335 INNER JOIN delivery_orders doe ON (doe.id = doi.delivery_order_id)
336 --INNER JOIN delivery_order_items_stock dois ON (doi.id = dois.delivery_order_item_id)
337 WHERE rl.from_table = 'orderitems'
338 AND rl.to_table = 'delivery_order_items'
340 Get all entries which were converted from orderitems to delivery_order_items (WHERE).
341 The persistent id are in rl, therefore we can fetch orderitems and delivery_order_items.
342 The join on delivery_orders (doe) is only needed for the current state of the delivery order (closed, delivered).
350 =item C<prepare_report>
352 =item C<make_filter_summary>
354 =item C<calc_qtys_price>
362 =item C<init_all_employees>
364 =item C<init_all_businesses>
370 Currently no foreign currencies and OrderItems with taxincluded are calculated / supported. The report can be easily extended
371 for the real stocked qty. The report is really easy to implement and customise if your model is focussed straight.
372 For long term maintaineance it would be wise to add more testcases for the conversion from orders to delivery_orders.
373 Right now record_links are tested only from document to document and the convert_invoice method (via task server) has a
374 test case with record_links items included. Furhtermore I personally dislike the calcs in the %columns_def, but for a quick report
375 this is ok, though if we redesign this further, the taxincluded / currency cases should be implemented as well.
380 Jan Büren E<lt>jan@kivitendo-premium.deE<gt> (based on DeliveryPlan.pm by Sven)