Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 4c29a7fc

Von Kivitendo Admin vor mehr als 7 Jahren hinzugefügt

  • ID 4c29a7fca22c28576da8f4f294734060d089e72d
  • Vorgänger e70f407f
  • Nachfolger f1b67648

Neuer Part Controller

Soll ic.pl komplett ersetzen.

Unterschiede anzeigen:

SL/Controller/Part.pm
8 8
use SL::Controller::Helper::GetModels;
9 9
use SL::Locale::String qw(t8);
10 10
use SL::JSON;
11
use List::Util qw(sum);
12
use SL::Helper::Flash;
13
use Data::Dumper;
14
use DateTime;
15
use SL::DB::History;
16
use SL::CVar;
17
use Carp;
11 18

  
12 19
use Rose::Object::MakeMethods::Generic (
13
  'scalar --get_set_init' => [ qw(parts models part) ],
20
  'scalar --get_set_init' => [ qw(parts models part p warehouses multi_items_models
21
                                  makemodels
22
                                  orphaned
23
                                  assortment assortment_items assembly assembly_items
24
                                  all_pricegroups all_translations all_partsgroups all_units
25
                                  all_buchungsgruppen all_payment_terms all_warehouses
26
                                  all_languages all_units all_pricefactors) ],
27
  'scalar'                => [ qw(warehouse bin) ],
14 28
);
15 29

  
16 30
# safety
17 31
__PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') },
18 32
                        except => [ qw(ajax_autocomplete part_picker_search part_picker_result) ]);
19 33

  
34
__PACKAGE__->run_before('check_part_id', only   => [ qw(edit delete) ]);
35

  
36
# actions for editing parts
37
#
38
sub action_add_part {
39
  my ($self, %params) = @_;
40

  
41
  $self->part( SL::DB::Part->new_part );
42
  $self->add;
43
};
44

  
45
sub action_add_service {
46
  my ($self, %params) = @_;
47

  
48
  $self->part( SL::DB::Part->new_service );
49
  $self->add;
50
};
51

  
52
sub action_add_assembly {
53
  my ($self, %params) = @_;
54

  
55
  $self->part( SL::DB::Part->new_assembly );
56
  $self->add;
57
};
58

  
59
sub action_add_assortment {
60
  my ($self, %params) = @_;
61

  
62
  $self->part( SL::DB::Part->new_assortment );
63
  $self->add;
64
};
65

  
66
sub action_add {
67
  my ($self) = @_;
68

  
69
  check_has_valid_part_type($::form->{part_type});
70

  
71
  $self->action_add_part       if $::form->{part_type} eq 'part';
72
  $self->action_add_service    if $::form->{part_type} eq 'service';
73
  $self->action_add_assembly   if $::form->{part_type} eq 'assembly';
74
  $self->action_add_assortment if $::form->{part_type} eq 'assortment';
75
};
76

  
77
sub action_save {
78
  my ($self, %params) = @_;
79

  
80
  # checks that depend only on submitted $::form
81
  $self->check_form or return $self->js->render;
82

  
83
  my $is_new = !$self->part->id; # $ part gets loaded here
84

  
85
  # check that the part hasn't been modified
86
  unless ( $is_new ) {
87
    $self->check_part_not_modified or
88
      return $self->js->error(t8('The document has been changed by another user. Please reopen it in another window and copy the changes to the new window'))->render;
89
  }
90

  
91
  if ( $is_new and !$::form->{part}{partnumber} ) {
92
    $self->check_next_transnumber_is_free or return $self->js->error(t8('The next partnumber in the number range already exists!'))->render;
93
  }
94

  
95
  $self->parse_form;
96

  
97
  my @errors = $self->part->validate;
98
  return $self->js->error(@errors)->render if @errors;
99

  
100
  # $self->part has been loaded, parsed and validated without errors and is ready to be saved
101
  $self->part->db->with_transaction(sub {
102

  
103
    if ( $params{save_as_new} ) {
104
      $self->part( $self->part->clone_and_reset_deep );
105
      $self->part->partnumber(undef); # will be assigned by _before_save_set_partnumber
106
    };
107

  
108
    $self->part->save(cascade => 1);
109

  
110
    SL::DB::History->new(
111
      trans_id    => $self->part->id,
112
      snumbers    => 'partnumber_' . $self->part->partnumber,
113
      employee_id => SL::DB::Manager::Employee->current->id,
114
      what_done   => 'part',
115
      addition    => 'SAVED',
116
    )->save();
117

  
118
    CVar->save_custom_variables(
119
        dbh          => $self->part->db->dbh,
120
        module       => 'IC',
121
        trans_id     => $self->part->id,
122
        variables    => $::form, # $::form->{cvar} would be nicer
123
        always_valid => 1,
124
    );
125

  
126
    1;
127
  }) or return $self->js->error(t8('The item couldn\'t be saved!') . " " . $self->part->db->error )->render;
128

  
129
  flash_later('info', $is_new ? t8('The item has been created.') : t8('The item has been saved.'));
130

  
131
  # reload item, this also resets last_modification!
132
  $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id);
133
}
134

  
135
sub action_save_as_new {
136
  my ($self) = @_;
137
  $self->action_save(save_as_new=>1);
138
}
139

  
140
sub action_delete {
141
  my ($self) = @_;
142

  
143
  my $db = $self->part->db; # $self->part has a get_set_init on $::form
144

  
145
  my $partnumber = $self->part->partnumber; # remember for history log
146

  
147
  $db->do_transaction(
148
    sub {
149

  
150
      # delete part, together with relationships that don't already
151
      # have an ON DELETE CASCADE, e.g. makemodel and translation.
152
      $self->part->delete(cascade => 1);
153

  
154
      SL::DB::History->new(
155
        trans_id    => $self->part->id,
156
        snumbers    => 'partnumber_' . $partnumber,
157
        employee_id => SL::DB::Manager::Employee->current->id,
158
        what_done   => 'part',
159
        addition    => 'DELETED',
160
      )->save();
161
      1;
162
  }) or return $self->js->error(t8('The item couldn\'t be deleted!') . " " . $self->part->db->error)->render;
163

  
164
  flash_later('info', t8('The item has been deleted.'));
165
  my @redirect_params = (
166
    controller => 'controller.pl',
167
    action => 'LoginScreen/user_login'
168
  );
169
  $self->redirect_to(@redirect_params);
170
}
171

  
172
sub action_use_as_new {
173
  my ($self, %params) = @_;
174

  
175
  my $oldpart = SL::DB::Manager::Part->find_by( id => $::form->{old_id}) or die "can't find old part";
176
  $::form->{oldpartnumber} = $oldpart->partnumber;
177

  
178
  $self->part($oldpart->clone_and_reset_deep);
179
  $self->parse_form;
180
  $self->part->partnumber(undef);
181

  
182
  $self->render_form;
183
}
184

  
185
sub action_edit {
186
  my ($self, %params) = @_;
187

  
188
  $self->render_form;
189
}
190

  
191
sub render_form {
192
  my ($self, %params) = @_;
193

  
194
  $self->_set_javascript;
195

  
196
  my (%assortment_vars, %assembly_vars);
197
  %assortment_vars = %{ $self->prepare_assortment_render_vars } if $self->part->is_assortment;
198
  %assembly_vars   = %{ $self->prepare_assembly_render_vars   } if $self->part->is_assembly;
199

  
200
  $params{CUSTOM_VARIABLES}  = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id);
201

  
202
  CVar->render_inputs('variables' => $params{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $self->part->partsgroup_id)
203
    if (scalar @{ $params{CUSTOM_VARIABLES} });
204

  
205
  my %title_hash = ( part       => t8('Edit Part'),
206
                     assembly   => t8('Edit Assembly'),
207
                     service    => t8('Edit Service'),
208
                     assortment => t8('Edit Assortment'),
209
                   );
210

  
211
  $self->part->prices([])       unless $self->part->prices;
212
  $self->part->translations([]) unless $self->part->translations;
213

  
214
  $self->render(
215
    'part/form',
216
    title             => $title_hash{$self->part->part_type},
217
    show_edit_buttons => $::auth->assert('part_service_assembly_edit'),
218
    %assortment_vars,
219
    %assembly_vars,
220
    translations_map  => { map { ($_->language_id   => $_) } @{$self->part->translations} },
221
    prices_map        => { map { ($_->pricegroup_id => $_) } @{$self->part->prices      } },
222
    oldpartnumber     => $::form->{oldpartnumber},
223
    old_id            => $::form->{old_id},
224
    %params,
225
  );
226
}
227

  
228
sub action_history {
229
  my ($self) = @_;
230

  
231
  my $history_entries = SL::DB::Part->new(id => $::form->{part}{id})->history_entries;
232
  $_[0]->render('part/history', { layout => 0 },
233
                                  history_entries => $history_entries);
234
}
235

  
236
sub action_update_item_totals {
237
  my ($self) = @_;
238

  
239
  my $part_type = $::form->{part_type};
240
  die unless $part_type =~ /^(assortment|assembly)$/;
241

  
242
  my $sellprice_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'sellcost');
243
  my $lastcost_sum  = $self->recalc_item_totals(part_type => $part_type, price_type => 'lastcost');
244

  
245
  my $sum_diff      = $sellprice_sum-$lastcost_sum;
246

  
247
  $self->js
248
    ->html('#items_sellprice_sum',       $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
249
    ->html('#items_lastcost_sum',        $::form->format_amount(\%::myconfig, $lastcost_sum,  2, 0))
250
    ->html('#items_sum_diff',            $::form->format_amount(\%::myconfig, $sum_diff,      2, 0))
251
    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
252
    ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $lastcost_sum,  2, 0))
253
    ->render();
254
}
255

  
256
sub action_add_multi_assortment_items {
257
  my ($self) = @_;
258

  
259
  my $item_objects = $self->parse_add_items_to_objects(part_type => 'assortment');
260
  my $html         = $self->render_assortment_items_to_html($item_objects);
261

  
262
  $self->js->run('kivi.Part.close_multi_items_dialog')
263
           ->append('#assortment_rows', $html)
264
           ->run('kivi.Part.renumber_positions')
265
           ->run('kivi.Part.assortment_recalc')
266
           ->render();
267
}
268

  
269
sub action_add_multi_assembly_items {
270
  my ($self) = @_;
271

  
272
  my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly');
273
  my $html         = $self->render_assembly_items_to_html($item_objects);
274

  
275
  $self->js->run('kivi.Part.close_multi_items_dialog')
276
           ->append('#assembly_rows', $html)
277
           ->run('kivi.Part.renumber_positions')
278
           ->run('kivi.Part.assembly_recalc')
279
           ->render();
280
}
281

  
282
sub action_add_assortment_item {
283
  my ($self, %params) = @_;
284

  
285
  validate_add_items() or return $self->js->error(t8("No part was selected."))->render;
286

  
287
  carp('Too many objects passed to add_assortment_item') if @{$::form->{add_items}} > 1;
288

  
289
  my $add_item_id = $::form->{add_items}->[0]->{parts_id};
290
  if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assortment_items } ) {
291
    return $self->js->flash('error', t8("This part has already been added."))->render;
292
  };
293

  
294
  my $number_of_items = scalar @{$self->assortment_items};
295
  my $item_objects    = $self->parse_add_items_to_objects(part_type => 'assortment');
296
  my $html            = $self->render_assortment_items_to_html($item_objects, $number_of_items);
297

  
298
  push(@{$self->assortment_items}, @{$item_objects});
299
  my $part = SL::DB::Part->new(part_type => 'assortment');
300
  $part->assortment_items(@{$self->assortment_items});
301
  my $items_sellprice_sum = $part->items_sellprice_sum;
302
  my $items_lastcost_sum  = $part->items_lastcost_sum;
303
  my $items_sum_diff      = $items_sellprice_sum - $items_lastcost_sum;
304

  
305
  $self->js
306
    ->append('#assortment_rows'        , $html)  # append in tbody
307
    ->val('.add_assortment_item_input' , '')
308
    ->run('kivi.Part.focus_last_assortment_input')
309
    ->html("#items_sellprice_sum", $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
310
    ->html("#items_lastcost_sum",  $::form->format_amount(\%::myconfig, $items_lastcost_sum,  2, 0))
311
    ->html("#items_sum_diff",      $::form->format_amount(\%::myconfig, $items_sum_diff,      2, 0))
312
    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
313
    ->html('#items_lastcost_sum_basic',  $::form->format_amount(\%::myconfig, $items_lastcost_sum,  2, 0))
314
    ->render;
315
}
316
sub action_add_assembly_item {
317
  my ($self) = @_;
318

  
319
  validate_add_items() or return $self->js->error(t8("No part was selected."))->render;
320

  
321
  carp('Too many objects passed to add_assembly_item') if @{$::form->{add_items}} > 1;
322

  
323
  my $add_item_id = $::form->{add_items}->[0]->{parts_id};
324
  my $duplicate_warning = 0; # duplicates are allowed, just warn
325
  if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assembly_items } ) {
326
    $duplicate_warning++;
327
  };
328

  
329
  my $number_of_items = scalar @{$self->assembly_items};
330
  my $item_objects    = $self->parse_add_items_to_objects(part_type => 'assembly');
331
  my $html            = $self->render_assembly_items_to_html($item_objects, $number_of_items);
332

  
333
  $self->js->flash('info', t8("This part has already been added.")) if $duplicate_warning;
334

  
335
  push(@{$self->assembly_items}, @{$item_objects});
336
  my $part = SL::DB::Part->new(part_type => 'assembly');
337
  $part->assemblies(@{$self->assembly_items});
338
  my $items_sellprice_sum = $part->items_sellprice_sum;
339
  my $items_lastcost_sum  = $part->items_lastcost_sum;
340
  my $items_sum_diff      = $items_sellprice_sum - $items_lastcost_sum;
341

  
342
  $self->js
343
    ->append('#assembly_rows', $html)  # append in tbody
344
    ->val('.add_assembly_item_input' , '')
345
    ->run('kivi.Part.focus_last_assembly_input')
346
    ->html('#items_sellprice_sum', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
347
    ->html('#items_lastcost_sum' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0))
348
    ->html('#items_sum_diff',      $::form->format_amount(\%::myconfig, $items_sum_diff     , 2, 0))
349
    ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
350
    ->html('#items_lastcost_sum_basic' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0))
351
    ->render;
352
}
353

  
354
sub action_show_multi_items_dialog {
355
  require SL::DB::PartsGroup;
356
  $_[0]->render('part/_multi_items_dialog', { layout => 0 },
357
                part_type => 'assortment',
358
                partfilter => '', # can I get at the current input of the partpicker here?
359
                all_partsgroups => SL::DB::Manager::PartsGroup->get_all);
360
}
361

  
362
sub action_multi_items_update_result {
363
  my $max_count = 100;
364

  
365
  $::form->{multi_items}->{filter}->{obsolete} = 0;
366

  
367
  my $count = $_[0]->multi_items_models->count;
368

  
369
  if ($count == 0) {
370
    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('No results.'));
371
    $_[0]->render($text, { layout => 0 });
372
  } elsif ($count > $max_count) {
373
    my $text = SL::Presenter::EscapedText->new(text => $::locale->text('Too many results (#1 from #2).', $count, $max_count));
374
    $_[0]->render($text, { layout => 0 });
375
  } else {
376
    my $multi_items = $_[0]->multi_items_models->get;
377
    $_[0]->render('part/_multi_items_result', { layout => 0 },
378
                  multi_items => $multi_items);
379
  }
380
}
381

  
382
sub action_add_makemodel_row {
383
  my ($self) = @_;
384

  
385
  my $vendor_id = $::form->{add_makemodel};
386

  
387
  my $vendor = SL::DB::Manager::Vendor->find_by(id => $vendor_id) or
388
    return $self->js->error(t8("No vendor selected or found!"))->render;
389

  
390
  if ( grep { $vendor_id == $_->make } @{ $self->makemodels } ) {
391
    $self->js->flash('info', t8("This vendor has already been added."));
392
  };
393

  
394
  my $position = scalar @{$self->makemodels} + 1;
395

  
396
  my $mm = SL::DB::MakeModel->new(# parts_id    => $::form->{part}->{id},
397
                                  make        => $vendor->id,
398
                                  model       => '',
399
                                  lastcost    => 0,
400
                                  sortorder    => $position,
401
                                 ) or die "Can't create MakeModel object";
402

  
403
  my $row_as_html = $self->p->render('part/_makemodel_row',
404
                                     makemodel => $mm,
405
                                     listrow   => $position % 2 ? 0 : 1,
406
  );
407

  
408
  # after selection focus on the model field in the row that was just added
409
  $self->js
410
    ->append('#makemodel_rows', $row_as_html)  # append in tbody
411
    ->val('.add_makemodel_input', '')
412
    ->run('kivi.Part.focus_last_makemodel_input')
413
    ->render;
414
}
415

  
416
sub action_reorder_items {
417
  my ($self) = @_;
418

  
419
  my $part_type = $::form->{part_type};
420

  
421
  my %sort_keys = (
422
    partnumber  => sub { $_[0]->part->partnumber },
423
    description => sub { $_[0]->part->description },
424
    qty         => sub { $_[0]->qty },
425
    sellprice   => sub { $_[0]->part->sellprice },
426
    lastcost    => sub { $_[0]->part->lastcost },
427
    partsgroup  => sub { $_[0]->part->partsgroup_id ? $_[0]->part->partsgroup->partsgroup : '' },
428
  );
429

  
430
  my $method = $sort_keys{$::form->{order_by}};
431

  
432
  my @items;
433
  if ($part_type eq 'assortment') {
434
    @items = @{ $self->assortment_items };
435
  } else {
436
    @items = @{ $self->assembly_items };
437
  };
438

  
439
  my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @items;
440
  if ($::form->{order_by} =~ /^(qty|sellprice|lastcost)$/) {
441
    if ($::form->{sort_dir}) {
442
      @to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
443
    } else {
444
      @to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
445
    }
446
  } else {
447
    if ($::form->{sort_dir}) {
448
      @to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
449
    } else {
450
      @to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
451
    }
452
  };
453

  
454
  $self->js->run('kivi.Part.redisplay_items', \@to_sort)->render;
455
}
456

  
457
sub action_warehouse_changed {
458
  my ($self) = @_;
459

  
460
  $self->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => $::form->{warehouse_id}));
461
  die unless ref($self->warehouse) eq 'SL::DB::Warehouse';
462

  
463
  if ( $self->warehouse->id and @{$self->warehouse->bins} ) {
464
    $self->bin($self->warehouse->bins->[0]);
465
    $self->js
466
      ->html('#bin', $self->build_bin_select)
467
      ->focus('#part_bin_id');
468
  } else {
469
    # no warehouse was selected, empty the bin field and reset the id
470
    $self->js
471
        ->val('#part_bin_id', undef)
472
        ->html('#bin', '');
473
  };
474

  
475
  return $self->js->render;
476
}
477

  
20 478
sub action_ajax_autocomplete {
21 479
  my ($self, %params) = @_;
22 480

  
......
85 543
  }
86 544
}
87 545

  
546
# helper functions
547
sub validate_add_items {
548
  scalar @{$::form->{add_items}};
549
}
550

  
551
sub prepare_assortment_render_vars {
552
  my ($self) = @_;
553

  
554
  my %vars = ( items_sellprice_sum => $self->part->items_sellprice_sum,
555
               items_lastcost_sum  => $self->part->items_lastcost_sum,
556
               assortment_html     => $self->render_assortment_items_to_html( \@{$self->part->items} ),
557
             );
558
  $vars{items_sum_diff} = $vars{items_sellprice_sum} - $vars{items_lastcost_sum};
559

  
560
  return \%vars;
561
}
562

  
563
sub prepare_assembly_render_vars {
564
  my ($self) = @_;
565

  
566
  my %vars = ( items_sellprice_sum => $self->part->items_sellprice_sum,
567
               items_lastcost_sum  => $self->part->items_lastcost_sum,
568
               assembly_html       => $self->render_assembly_items_to_html( \@{ $self->part->items } ),
569
             );
570
  $vars{items_sum_diff} = $vars{items_sellprice_sum} - $vars{items_lastcost_sum};
571

  
572
  return \%vars;
573
}
574

  
575
sub add {
576
  my ($self) = @_;
577

  
578
  check_has_valid_part_type($self->part->part_type);
579

  
580
  $self->_set_javascript;
581

  
582
  my %title_hash = ( part       => t8('Add Part'),
583
                     assembly   => t8('Add Assembly'),
584
                     service    => t8('Add Service'),
585
                     assortment => t8('Add Assortment'),
586
                   );
587

  
588
  $self->render(
589
    'part/form',
590
    title             => $title_hash{$self->part->part_type},
591
    show_edit_buttons => $::auth->assert('part_service_assembly_edit'),
592
  );
593
}
594

  
595

  
596
sub _set_javascript {
597
  my ($self) = @_;
598
  $::request->layout->use_javascript("${_}.js")  for qw(kivi.Part kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery);
599
  $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id;
600
}
601

  
602
sub recalc_item_totals {
603
  my ($self, %params) = @_;
604

  
605
  if ( $params{part_type} eq 'assortment' ) {
606
    return 0 unless scalar @{$self->assortment_items};
607
  } elsif ( $params{part_type} eq 'assembly' ) {
608
    return 0 unless scalar @{$self->assembly_items};
609
  } else {
610
    carp "can only calculate sum for assortments and assemblies";
611
  };
612

  
613
  my $part = SL::DB::Part->new(part_type => $params{part_type});
614
  if ( $part->is_assortment ) {
615
    $part->assortment_items( @{$self->assortment_items} );
616
    if ( $params{price_type} eq 'lastcost' ) {
617
      return $part->items_lastcost_sum;
618
    } else {
619
      if ( $params{pricegroup_id} ) {
620
        return $part->items_sellprice_sum(pricegroup_id => $params{pricegroup_id});
621
      } else {
622
        return $part->items_sellprice_sum;
623
      };
624
    }
625
  } elsif ( $part->is_assembly ) {
626
    $part->assemblies( @{$self->assembly_items} );
627
    if ( $params{price_type} eq 'lastcost' ) {
628
      return $part->items_lastcost_sum;
629
    } else {
630
      return $part->items_sellprice_sum;
631
    }
632
  }
633
}
634

  
635
sub check_part_not_modified {
636
  my ($self) = @_;
637

  
638
  return !($::form->{last_modification} && ($self->part->last_modification ne $::form->{last_modification}));
639

  
640
}
641

  
642
sub parse_form {
643
  my ($self) = @_;
644

  
645
  my $is_new = !$self->part->id;
646

  
647
  my $params = delete($::form->{part}) || { };
648

  
649
  delete $params->{id};
650
  # never overwrite existing partnumber, should be a read-only field anyway
651
  delete $params->{partnumber} if $self->part->partnumber;
652
  $self->part->assign_attributes(%{ $params});
653
  $self->part->bin_id(undef) unless $self->part->warehouse_id;
654

  
655
  # Only reset items ([]) and rewrite from form if $::form->{assortment_items} isn't empty. This
656
  # will be the case for used assortments when saving, or when a used assortment
657
  # is "used as new"
658
  if ( $self->part->is_assortment and $::form->{assortment_items} and scalar @{$::form->{assortment_items}}) {
659
    $self->part->assortment_items([]);
660
    $self->part->add_assortment_items(@{$self->assortment_items}); # assortment_items has a get_set_init
661
  };
662

  
663
  if ( $self->part->is_assembly and $::form->{assembly_items} and @{$::form->{assembly_items}} ) {
664
    $self->part->assemblies([]); # completely rewrite assortments each time
665
    $self->part->add_assemblies( @{ $self->assembly_items } );
666
  };
667

  
668
  $self->part->translations([]);
669
  $self->parse_form_translations;
670

  
671
  $self->part->prices([]);
672
  $self->parse_form_prices;
673

  
674
  $self->parse_form_makemodels;
675
}
676

  
677
sub parse_form_prices {
678
  my ($self) = @_;
679
  # only save prices > 0
680
  my $prices = delete($::form->{prices}) || [];
681
  foreach my $price ( @{$prices} ) {
682
    my $sellprice = $::form->parse_amount(\%::myconfig, $price->{price});
683
    next unless $sellprice > 0; # skip negative prices as well
684
    my $p = SL::DB::Price->new(parts_id      => $self->part->id,
685
                               pricegroup_id => $price->{pricegroup_id},
686
                               price         => $sellprice,
687
                              );
688
    $self->part->add_prices($p);
689
  };
690
}
691

  
692
sub parse_form_translations {
693
  my ($self) = @_;
694
  # don't add empty translations
695
  my $translations = delete($::form->{translations}) || [];
696
  foreach my $translation ( @{$translations} ) {
697
    next unless $translation->{translation};
698
    my $t = SL::DB::Translation->new( %{$translation} ) or die "Can't create translation";
699
    $self->part->add_translations( $translation );
700
  };
701
}
702

  
703
sub parse_form_makemodels {
704
  my ($self) = @_;
705

  
706
  my $makemodels_map;
707
  if ( $self->part->makemodels ) { # check for new parts or parts without makemodels
708
    $makemodels_map = { map { $_->id => Rose::DB::Object::Helpers::clone($_) } @{$self->part->makemodels} };
709
  };
710

  
711
  $self->part->makemodels([]);
712

  
713
  my $position = 0;
714
  my $makemodels = delete($::form->{makemodels}) || [];
715
  foreach my $makemodel ( @{$makemodels} ) {
716
    next unless $makemodel->{make};
717
    $position++;
718
    my $vendor = SL::DB::Manager::Vendor->find_by(id => $makemodel->{make}) || die "Can't find vendor from make";
719

  
720
    my $mm = SL::DB::MakeModel->new( # parts_id   => $self->part->id, # will be assigned by row add_makemodels
721
                                     id         => $makemodel->{id},
722
                                     make       => $makemodel->{make},
723
                                     model      => $makemodel->{model} || '',
724
                                     lastcost   => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number}),
725
                                     sortorder  => $position,
726
                                   );
727
    if ($makemodels_map->{$mm->id} && !$makemodels_map->{$mm->id}->lastupdate && $makemodels_map->{$mm->id}->lastcost == 0 && $mm->lastcost == 0) {
728
      # lastupdate isn't set, original lastcost is 0 and new lastcost is 0
729
      # don't change lastupdate
730
    } elsif ( !$makemodels_map->{$mm->id} && $mm->lastcost == 0 ) {
731
      # new makemodel, no lastcost entered, leave lastupdate empty
732
    } elsif ($makemodels_map->{$mm->id} && $makemodels_map->{$mm->id}->lastcost == $mm->lastcost) {
733
      # lastcost hasn't changed, use original lastupdate
734
      $mm->lastupdate($makemodels_map->{$mm->id}->lastupdate);
735
    } else {
736
      $mm->lastupdate(DateTime->now);
737
    };
738
    $self->part->makemodel( scalar @{$self->part->makemodels} ? 1 : 0 ); # do we need this boolean anymore?
739
    $self->part->add_makemodels($mm);
740
  };
741
}
742

  
743
sub build_bin_select {
744
  $_[0]->p->select_tag('part.bin_id', [ $_[0]->warehouse->bins ],
745
    title_key => 'description',
746
    default   => $_[0]->bin->id,
747
  );
748
}
749

  
750
# get_set_inits for partpicker
751

  
88 752
sub init_parts {
89 753
  if ($::form->{no_paginate}) {
90 754
    $_[0]->models->disable_plugin('paginated');
......
93 757
  $_[0]->models->get;
94 758
}
95 759

  
760
# get_set_inits for part controller
96 761
sub init_part {
97
  SL::DB::Part->new(id => $::form->{id} || $::form->{part}{id})->load;
762
  my ($self) = @_;
763

  
764
  # used by edit, save, delete and add
765

  
766
  if ( $::form->{part}{id} ) {
767
    return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels prices translations partsgroup) ]);
768
  } else {
769
    die "part_type missing" unless $::form->{part}{part_type};
770
    return SL::DB::Part->new(part_type => $::form->{part}{part_type});
771
  };
772
}
773

  
774
sub init_orphaned {
775
  my ($self) = @_;
776
  return $self->part->orphaned;
98 777
}
99 778

  
100 779
sub init_models {
......
114 793
  );
115 794
}
116 795

  
796
sub init_p {
797
  SL::Presenter->get;
798
}
799

  
800

  
801
sub init_assortment_items {
802
  # this init is used while saving and whenever assortments change dynamically
803
  my ($self) = @_;
804
  my $position = 0;
805
  my @array;
806
  my $assortment_items = delete($::form->{assortment_items}) || [];
807
  foreach my $assortment_item ( @{$assortment_items} ) {
808
    next unless $assortment_item->{parts_id};
809
    $position++;
810
    my $part = SL::DB::Manager::Part->find_by(id => $assortment_item->{parts_id}) || die "Can't determine item to be added";
811
    my $ai = SL::DB::AssortmentItem->new( parts_id      => $part->id,
812
                                          qty           => $::form->parse_amount(\%::myconfig, $assortment_item->{qty_as_number}),
813
                                          charge        => $assortment_item->{charge},
814
                                          unit          => $assortment_item->{unit} || $part->unit,
815
                                          position      => $position,
816
    );
817

  
818
    push(@array, $ai);
819
  };
820
  return \@array;
821
}
822

  
823
sub init_makemodels {
824
  my ($self) = @_;
825

  
826
  my $position = 0;
827
  my @makemodel_array = ();
828
  my $makemodels = delete($::form->{makemodels}) || [];
829

  
830
  foreach my $makemodel ( @{$makemodels} ) {
831
    next unless $makemodel->{make};
832
    $position++;
833
    my $mm = SL::DB::MakeModel->new( # parts_id   => $self->part->id, # will be assigned by row add_makemodels
834
                                    id        => $makemodel->{id},
835
                                    make      => $makemodel->{make},
836
                                    model     => $makemodel->{model} || '',
837
                                    lastcost  => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number} || 0),
838
                                    sortorder => $position,
839
                                  ) or die "Can't create mm";
840
    # $mm->id($makemodel->{id}) if $makemodel->{id};
841
    push(@makemodel_array, $mm);
842
  };
843
  return \@makemodel_array;
844
}
845

  
846
sub init_assembly_items {
847
  my ($self) = @_;
848
  my $position = 0;
849
  my @array;
850
  my $assembly_items = delete($::form->{assembly_items}) || [];
851
  foreach my $assembly_item ( @{$assembly_items} ) {
852
    next unless $assembly_item->{parts_id};
853
    $position++;
854
    my $part = SL::DB::Manager::Part->find_by(id => $assembly_item->{parts_id}) || die "Can't determine item to be added";
855
    my $ai = SL::DB::Assembly->new(parts_id    => $part->id,
856
                                   bom         => $assembly_item->{bom},
857
                                   qty         => $::form->parse_amount(\%::myconfig, $assembly_item->{qty_as_number}),
858
                                   position    => $position,
859
                                  );
860
    push(@array, $ai);
861
  };
862
  return \@array;
863
}
864

  
865
sub init_all_warehouses {
866
  my ($self) = @_;
867
  SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef, id => $self->part->warehouse_id ] ]);
868
}
869

  
870
sub init_all_languages {
871
  SL::DB::Manager::Language->get_all_sorted;
872
}
873

  
874
sub init_all_partsgroups {
875
  SL::DB::Manager::PartsGroup->get_all_sorted;
876
}
877

  
878
sub init_all_buchungsgruppen {
879
  my ($self) = @_;
880
  if ( $self->part->orphaned ) {
881
    return SL::DB::Manager::Buchungsgruppe->get_all_sorted;
882
  } else {
883
    return SL::DB::Manager::Buchungsgruppe->get_all(where => [ id => $self->part->buchungsgruppen_id ]);
884
  }
885
}
886

  
887
sub init_all_units {
888
  my ($self) = @_;
889
  if ( $self->part->orphaned ) {
890
    return SL::DB::Manager::Unit->get_all_sorted;
891
  } else {
892
    return SL::DB::Manager::Unit->get_all(where => [ unit => $self->part->unit ]);
893
  }
894
}
895

  
896
sub init_all_payment_terms {
897
  SL::DB::Manager::PaymentTerm->get_all_sorted;
898
}
899

  
900
sub init_all_price_factors {
901
  SL::DB::Manager::PriceFactor->get_all_sorted;
902
}
903

  
904
sub init_all_pricegroups {
905
  SL::DB::Manager::Pricegroup->get_all_sorted;
906
}
907

  
908
# model used to filter/display the parts in the multi-items dialog
909
sub init_multi_items_models {
910
  SL::Controller::Helper::GetModels->new(
911
    controller     => $_[0],
912
    model          => 'Part',
913
    with_objects   => [ qw(unit_obj partsgroup) ],
914
    disable_plugin => 'paginated',
915
    source         => $::form->{multi_items},
916
    sorted         => {
917
      _default    => {
918
        by  => 'partnumber',
919
        dir => 1,
920
      },
921
      partnumber  => t8('Partnumber'),
922
      description => t8('Description')}
923
  );
924
}
925

  
926
# simple checks to run on $::form before saving
927

  
928
sub form_check_part_description_exists {
929
  my ($self) = @_;
930

  
931
  return 1 if $::form->{part}{description};
932

  
933
  $self->js->flash('error', t8('Part Description missing!'))
934
           ->run('kivi.Part.set_tab_active_by_name', 'basic_data')
935
           ->focus('#part_description');
936
  return 0;
937
}
938

  
939
sub form_check_assortment_items_exist {
940
  my ($self) = @_;
941

  
942
  return 1 unless $::form->{part}{part_type} eq 'assortment';
943
  # skip check for existing parts that have been used
944
  return 1 if ($self->part->id and !$self->part->orphaned);
945

  
946
  # new or orphaned parts must have items in $::form->{assortment_items}
947
  unless ( $::form->{assortment_items} and scalar @{$::form->{assortment_items}} ) {
948
    $self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
949
             ->focus('#add_assortment_item_name')
950
             ->flash('error', t8('The assortment doesn\'t have any items.'));
951
    return 0;
952
  };
953
  return 1;
954
}
955

  
956
sub form_check_assortment_items_unique {
957
  my ($self) = @_;
958

  
959
  return 1 unless $::form->{part}{part_type} eq 'assortment';
960

  
961
  my %duplicate_elements;
962
  my %count;
963
  for (map { $_->{parts_id} } @{$::form->{assortment_items}}) {
964
    $duplicate_elements{$_}++ if $count{$_}++;
965
  };
966

  
967
  if ( keys %duplicate_elements ) {
968
    $self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
969
             ->flash('error', t8('There are duplicate assortment items'));
970
    return 0;
971
  };
972
  return 1;
973
}
974

  
975
sub form_check_assembly_items_exist {
976
  my ($self) = @_;
977

  
978
  return 1 unless $::form->{part}->{part_type} eq 'assembly';
979

  
980
  unless ( $::form->{assembly_items} and scalar @{$::form->{assembly_items}} ) {
981
    $self->js->run('kivi.Part.set_tab_active_by_name', 'assembly_tab')
982
             ->focus('#add_assembly_item_name')
983
             ->flash('error', t8('The assembly doesn\'t have any items.'));
984
    return 0;
985
  };
986
  return 1;
987
}
988

  
989
sub form_check_partnumber_is_unique {
990
  my ($self) = @_;
991

  
992
  if ( !$::form->{part}{id} and $::form->{part}{partnumber} ) {
993
    my $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $::form->{part}{partnumber} ]);
994
    if ( $count ) {
995
      $self->js->flash('error', t8('The partnumber already exists!'))
996
               ->focus('#part_description');
997
      return 0;
998
    };
999
  };
1000
  return 1;
1001
}
1002

  
1003
# general checking functions
1004
sub check_next_transnumber_is_free {
1005
  my ($self) = @_;
1006

  
1007
  my ($next_transnumber, $count);
1008
  $self->part->db->with_transaction(sub {
1009
    $next_transnumber = $self->part->get_next_trans_number;
1010
    $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $next_transnumber ]);
1011
    return 1;
1012
  }) or die $@;
1013
  $count ? return 0 : return 1;
1014
}
1015

  
1016
sub check_part_id {
1017
  die t8("Can't load item without a valid part.id") . "\n" unless $::form->{part}{id};
1018
}
1019

  
1020
sub check_form {
1021
  my ($self) = @_;
1022

  
1023
  $self->form_check_part_description_exists || return 0;
1024
  $self->form_check_assortment_items_exist  || return 0;
1025
  $self->form_check_assortment_items_unique || return 0;
1026
  $self->form_check_assembly_items_exist    || return 0;
1027
  $self->form_check_partnumber_is_unique    || return 0;
1028

  
1029
  return 1;
1030
}
1031

  
1032
sub check_has_valid_part_type {
1033
  die "invalid part_type" unless $_[0] =~ /^(part|service|assembly|assortment)$/;
1034
}
1035

  
1036
sub render_assortment_items_to_html {
1037
  my ($self, $assortment_items, $number_of_items) = @_;
1038

  
1039
  my $position = $number_of_items + 1;
1040
  my $html;
1041
  foreach my $ai (@$assortment_items) {
1042
    $html .= $self->p->render('part/_assortment_row',
1043
                              PART     => $self->part,
1044
                              orphaned => $self->orphaned,
1045
                              ITEM     => $ai,
1046
                              listrow  => $position % 2 ? 1 : 0,
1047
                              position => $position, # for legacy assemblies
1048
                             );
1049
    $position++;
1050
  };
1051
  return $html;
1052
}
1053

  
1054
sub render_assembly_items_to_html {
1055
  my ($self, $assembly_items, $number_of_items) = @_;
1056

  
1057
  my $position = $number_of_items + 1;
1058
  my $html;
1059
  foreach my $ai (@{$assembly_items}) {
1060
    $html .= $self->p->render('part/_assembly_row',
1061
                              PART     => $self->part,
1062
                              orphaned => $self->orphaned,
1063
                              ITEM     => $ai,
1064
                              listrow  => $position % 2 ? 1 : 0,
1065
                              position => $position, # for legacy assemblies
1066
                             );
1067
    $position++;
1068
  };
1069
  return $html;
1070
}
1071

  
1072
sub parse_add_items_to_objects {
1073
  my ($self, %params) = @_;
1074
  my $part_type = $params{part_type};
1075
  die unless $params{part_type} =~ /^(assortment|assembly)$/;
1076
  my $position = $params{position} || 1;
1077

  
1078
  my @add_items = grep { $_->{qty_as_number} } @{ $::form->{add_items} };
1079

  
1080
  my @item_objects;
1081
  foreach my $item ( @add_items ) {
1082
    my $part = SL::DB::Manager::Part->find_by(id => $item->{parts_id}) || die "Can't load part";
1083
    my $ai;
1084
    if ( $part_type eq 'assortment' ) {
1085
       $ai = SL::DB::AssortmentItem->new(part          => $part,
1086
                                         qty           => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
1087
                                         unit          => $part->unit, # TODO: $item->{unit} || $part->unit
1088
                                         position      => $position,
1089
                                        ) or die "Can't create AssortmentItem from item";
1090
    } elsif ( $part_type eq 'assembly' ) {
1091
      $ai = SL::DB::Assembly->new(parts_id    => $part->id,
1092
                                 # id          => $self->assembly->id, # will be set on save
1093
                                 qty         => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
1094
                                 bom         => 0, # default when adding: no bom
1095
                                 position    => $position,
1096
                                );
1097
    } else {
1098
      die "part_type must be assortment or assembly";
1099
    }
1100
    push(@item_objects, $ai);
1101
    $position++;
1102
  };
1103

  
1104
  return \@item_objects;
1105
}
1106

  
117 1107
1;
1108

  
1109
__END__
1110

  
1111
=encoding utf-8
1112

  
1113
=head1 NAME
1114

  
1115
SL::Controller::Part - Part CRUD controller
1116

  
1117
=head1 DESCRIPTION
1118

  
1119
Controller for adding/editing/saving/deleting parts.
1120

  
1121
All the relations are loaded at once and saving the part, adding a history
1122
entry and saving CVars happens inside one transaction.  When saving the old
1123
relations are deleted and written as new to the database.
1124

  
1125
Relations for parts:
1126

  
1127
=over 2
1128

  
1129
=item makemodels
1130

  
1131
=item translations
1132

  
1133
=item assembly items
1134

  
1135
=item assortment items
1136

  
1137
=item prices
1138

  
1139
=back
1140

  
1141
=head1 PART_TYPES
1142

  
1143
There are 4 different part types:
1144

  
1145
=over 4
1146

  
1147
=item C<part>
1148

  
1149
The "default" part type.
1150

  
1151
inventory_accno_id is set.
1152

  
1153
=item C<service>
1154

  
1155
Services can't be stocked.
1156

  
1157
inventory_accno_id isn't set.
1158

  
1159
=item C<assembly>
1160

  
1161
Assemblies consist of other parts, services, assemblies or assortments. They
1162
aren't meant to be bought, only sold. To add assemblies to stock you typically
1163
have to make them, which reduces the stock by its respective components. Once
1164
an assembly item has been created there is currently no way to "disassemble" it
1165
again. An assembly item can appear several times in one assembly. An assmbly is
1166
sold as one item with a defined sellprice and lastcost. If the component prices
1167
change the assortment price remains the same. The assembly items may be printed
1168
in a record if the item's "bom" is set.
1169

  
1170
=item C<assortment>
1171

  
1172
Similar to assembly, but each assortment item may only appear once per
1173
assortment. When selling an assortment the assortment items are added to the
1174
record together with the assortment, which is added with sellprice 0.
1175

  
1176
Technically an assortment doesn't have a sellprice, but rather the sellprice is
1177
determined by the sum of the current assortment item prices when the assortment
1178
is added to a record. This also means that price rules and customer discounts
1179
will be applied to the assortment items.
1180

  
1181
Once the assortment items have been added they may be modified or deleted, just
1182
as if they had been added manually, the individual assortment items aren't
1183
linked to the assortment or the other assortment items in any way.
1184

  
1185
=back
1186

  
1187
=head1 URL ACTIONS
1188

  
1189
=over 4
1190

  
1191
=item C<action_add_part>
1192

  
1193
=item C<action_add_service>
1194

  
1195
=item C<action_add_assembly>
1196

  
1197
=item C<action_add_assortment>
1198

  
1199
=item C<action_add PART_TYPE>
1200

  
1201
An alternative to the action_add_$PART_TYPE actions, takes the mandatory
1202
parameter part_type as an action. Example:
1203

  
1204
  controller.pl?action=Part/add&part_type=service
1205

  
1206
=item C<action_save>
1207

  
1208
Saves the current part and then reloads the edit page for the part.
1209

  
1210
=item C<action_use_as_new>
1211

  
1212
Takes the information from the current part, plus any modifications made on the
1213
page, and creates a new edit page that is ready to be saved. The partnumber is
1214
set empty, so a new partnumber from the number range will be used if the user
1215
doesn't enter one manually.
1216

  
1217
Unsaved changes to the original part aren't updated.
1218

  
1219
The part type cannot be changed in this way.
1220

  
1221
=item C<action_delete>
1222

  
1223
Deletes the current part and then redirects to the main page, there is no
1224
callback.
1225

  
1226
The delete button only appears if the part is 'orphaned', according to
1227
SL::DB::Part orphaned.
1228

  
1229
The part can't be deleted if it appears in invoices, orders, delivery orders,
1230
the inventory, or is part of an assembly or assortment.
1231

  
1232
If the part is deleted its relations prices, makdemodel, assembly,
1233
assortment_items and translation are are also deleted via DELETE ON CASCADE.
1234

  
1235
Before this controller items that appeared in inventory didn't count as
1236
orphaned and could be deleted and the inventory entries were also deleted, this
1237
"feature" hasn't been implemented.
1238

  
1239
=item C<action_edit part.id>
1240

  
1241
Load and display a part for editing.
1242

  
1243
  controller.pl?action=Part/edit&part.id=12345
1244

  
1245
Passing the part id is mandatory, and the parameter is "part.id", not "id".
1246

  
1247
=back
1248

  
1249
=head1 BUTTON ACTIONS
1250

  
1251
=over 4
1252

  
1253
=item C<history>
1254

  
1255
Opens a popup displaying all the history entries. Once a new history controller
1256
is written the button could link there instead, with the part already selected.
1257

  
1258
=back
1259

  
1260
=head1 AJAX ACTIONS
1261

  
1262
=over 4
1263

  
1264
=item C<action_update_item_totals>
1265

  
1266
Is called whenever an element with the .recalc class loses focus, e.g. the qty
1267
amount of an item changes. The sum of all sellprices and lastcosts is
1268
calculated and the totals updated. Uses C<recalc_item_totals>.
1269

  
1270
=item C<action_add_assortment_item>
1271

  
1272
Adds a new assortment item from a part picker seleciton to the assortment item list
1273

  
1274
If the item already exists in the assortment the item isn't added and a Flash
1275
error shown.
1276

  
1277
Rather than running kivi.Part.renumber_positions and kivi.Part.assembly_recalc
1278
after adding each new item, add the new object to the item objects that were
1279
already parsed, calculate totals via a dummy part then update the row and the
1280
totals.
1281

  
1282
=item C<action_add_assembly_item>
1283

  
1284
Adds a new assembly item from a part picker seleciton to the assembly item list
1285

  
1286
If the item already exists in the assembly a flash info is generated, but the
1287
item is added.
1288

  
1289
Rather than running kivi.Part.renumber_positions and kivi.Part.assembly_recalc
1290
after adding each new item, add the new object to the item objects that were
1291
already parsed, calculate totals via a dummy part then update the row and the
1292
totals.
1293

  
1294
=item C<action_add_multi_assortment_items>
1295

  
1296
Parses the items to be added from the form generated by the multi input and
1297
appends the html of the tr-rows to the assortment item table. Afterwards all
1298
assortment items are renumbered and the sums recalculated via
1299
kivi.Part.renumber_positions and kivi.Part.assortment_recalc.
1300

  
1301
=item C<action_add_multi_assembly_items>
1302

  
1303
Parses the items to be added from the form generated by the multi input and
1304
appends the html of the tr-rows to the assembly item table. Afterwards all
1305
assembly items are renumbered and the sums recalculated via
1306
kivi.Part.renumber_positions and kivi.Part.assembly_recalc.
1307

  
1308
=item C<action_show_multi_items_dialog>
1309

  
1310
=item C<action_multi_items_update_result>
1311

  
1312
=item C<action_add_makemodel_row>
1313

  
1314
Add a new makemodel row with the vendor that was selected via the vendor
1315
picker.
1316

  
1317
Checks the already existing makemodels and warns if a row with that vendor
1318
already exists. Currently it is possible to have duplicate vendor rows.
1319

  
1320
=item C<action_reorder_items>
1321

  
1322
Sorts the item table for assembly or assortment items.
1323

  
1324
=item C<action_warehouse_changed>
1325

  
1326
=back
1327

  
1328
=head1 ACTIONS part picker
1329

  
1330
=over 4
1331

  
1332
=item C<action_ajax_autocomplete>
1333

  
1334
=item C<action_test_page>
1335

  
1336
=item C<action_part_picker_search>
1337

  
1338
=item C<action_part_picker_result>
1339

  
1340
=item C<action_show>
1341

  
1342
=back
1343

  
1344
=head1 FORM CHECKS
1345

  
1346
=over 2
1347

  
1348
=item C<check_form>
1349

  
1350
Calls some simple checks that test the submitted $::form for obvious errors.
1351
Return 1 if all the tests were successfull, 0 as soon as one test fails.
1352

  
1353
Errors from the failed tests are stored as ClientJS actions in $self->js. In
1354
some cases extra actions are taken, e.g. if the part description is missing the
1355
basic data tab is selected and the description input field is focussed.
1356

  
1357
=back
1358

  
1359
=over 4
1360

  
1361
=item C<form_check_part_description_exists>
1362

  
1363
=item C<form_check_assortment_items_exist>
1364

  
1365
=item C<form_check_assortment_items_unique>
1366

  
1367
=item C<form_check_assembly_items_exist>
1368

  
1369
=item C<form_check_partnumber_is_unique>
1370

  
1371
=back
1372

  
1373
=head1 HELPER FUNCTIONS
1374

  
1375
=over 4
1376

  
1377
=item C<parse_form>
1378

  
1379
When submitting the form for saving, parses the transmitted form. Expects the
1380
following data:
1381

  
1382
 $::form->{part}
1383
 $::form->{makemodels}
1384
 $::form->{translations}
1385
 $::form->{prices}
1386
 $::form->{assemblies}
1387
 $::form->{assortments}
1388

  
1389
CVar data is currently stored directly in $::form, e.g. $::form->{cvar_size}.
1390

  
1391
=item C<recalc_item_totals %params>
1392

  
1393
Helper function for calculating the total lastcost and sellprice for assemblies
1394
or assortments according to their items, which are parsed from the current
1395
$::form.
1396

  
1397
Is called whenever the qty of an item is changed or items are deleted.
1398

  
1399
Takes two params:
1400

  
1401
* part_type : 'assortment' or 'assembly' (mandatory)
1402

  
1403
* price_type: 'lastcost' or 'sellprice', default is 'sellprice'
1404

  
1405
Depending on the price_type the lastcost sum or sellprice sum is returned.
1406

  
1407
Doesn't work for recursive items.
1408

  
1409
=back
1410

  
1411
=head1 GET SET INITS
1412

  
1413
There are get_set_inits for
1414

  
1415
* assembly items
1416

  
1417
* assortment items
1418

  
1419
* makemodels
1420

  
1421
which parse $::form and automatically create an array of objects.
1422

  
1423
These inits are used during saving and each time a new element is added.
1424

  
1425
=over 4
1426

  
1427
=item C<init_makemodels>
1428

  
1429
Parses $::form->{makemodels}, creates an array of makemodel objects and stores them in
1430
$self->part->makemodels, ready to be saved.
1431

  
1432
Used for saving parts and adding new makemodel rows.
1433

  
1434
=item C<parse_add_items_to_objects PART_TYPE>
1435

  
1436
Parses the resulting form from either the part-picker submit or the multi-item
1437
submit, and creates an arrayref of assortment_item or assembly objects, that
1438
can be rendered via C<render_assortment_items_to_html> or
1439
C<render_assembly_items_to_html>.
1440

  
1441
Mandatory param: part_type: assortment or assembly (the resulting html will differ)
1442
Optional param: position (used for numbering and listrow class)
1443

  
1444
=item C<render_assortment_items_to_html ITEM_OBJECTS>
1445

  
1446
Takes an array_ref of assortment_items, and generates tables rows ready for
1447
adding to the assortment table.  Is used when a part is loaded, or whenever new
1448
assortment items are added.
1449

  
1450
=item C<parse_form_makemodels>
1451

  
1452
Makemodels can't just be overwritten, because of the field "lastupdate", that
1453
remembers when the lastcost for that vendor changed the last time.
1454

  
1455
So the original values are cloned and remembered, so we can compare if lastcost
1456
was changed in $::form, and keep or update lastupdate.
1457

  
1458
lastcost isn't updated until the first time it was saved with a value, until
1459
then it is empty.
1460

  
1461
Also a boolean "makemodel" needs to be written in parts, depending on whether
1462
makemodel entries exist or not.
1463

  
1464
We still need init_makemodels for when we open the part for editing.
1465

  
1466
=back
1467

  
1468
=head1 TODO
1469

  
1470
=over 4
1471

  
1472
=item *
1473

  
1474
It should be possible to jump to the edit page in a specific tab
1475

  
1476
=item *
1477

  
1478
Support callbacks, e.g. creating a new part from within an order, and jumping
1479
back to the order again afterwards.
1480

  
1481
=item *
1482

  
1483
Support units when adding assembly items or assortment items. Currently the
1484
default unit of the item is always used.
1485

  
1486
=item *
1487

  
1488
Calculate sellprice and lastcost totals recursively, in case e.g. an assembly
1489
consists of other assemblies.
1490

  
1491
=back
1492

  
1493
=head1 AUTHOR
1494

  
1495
G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
1496

  
1497
=cut

Auch abrufbar als: Unified diff