]> wagnertech.de Git - mfinanz.git/blob - SL/Model/Record.pm
date error in mapping
[mfinanz.git] / SL / Model / Record.pm
1 package SL::Model::Record;
2
3 use strict;
4
5 use Carp;
6
7 use SL::DB::Employee;
8 use SL::DB::Order;
9 use SL::DB::DeliveryOrder;
10 use SL::DB::Reclamation;
11 use SL::DB::RequirementSpecOrder;
12 use SL::DB::History;
13 use SL::DB::Invoice;
14 use SL::DB::Status;
15 use SL::DB::ValidityToken;
16 use SL::DB::Order::TypeData qw(:types);
17 use SL::DB::DeliveryOrder::TypeData qw(:types);
18 use SL::DB::Reclamation::TypeData qw(:types);
19 use SL::DB::Helper::Record qw(get_class_from_type);
20
21 use SL::Util qw(trim);
22 use SL::Locale::String qw(t8);
23 use SL::PriceSource;
24
25
26 sub update_after_new {
27   my ($class, $new_record, %flags) = @_;
28
29   $new_record->transdate(DateTime->now_local());
30
31   my $default_reqdate = $new_record->type_data->defaults('reqdate');
32   $new_record->reqdate($default_reqdate);
33
34   return $new_record;
35 }
36
37 sub update_after_customer_vendor_change {
38   my ($class, $record) = @_;
39   my $new_customervendor = $record->customervendor;
40
41   $record->$_($new_customervendor->$_) for (qw(
42     taxzone_id payment_id delivery_term_id currency_id language_id
43     ));
44
45   $record->intnotes($new_customervendor->notes);
46
47   return $record if !$record->is_sales;
48   if ($record->is_sales) {
49     my $new_customer = $new_customervendor;
50     $record->salesman_id($new_customer->salesman_id
51       || SL::DB::Manager::Employee->current->id);
52     $record->taxincluded(defined($new_customer->taxincluded_checked)
53       ? $new_customer->taxincluded_checked
54       : $::myconfig{taxincluded_checked});
55     if ($record->type_data->features('price_tax')) {
56       my $address = $new_customer->default_billing_address;;
57       $record->billing_address_id($address ? $address->id : undef);
58     }
59   }
60
61   return $record;
62 }
63
64 sub get_record {
65   my ($class, $type, $id) = @_;
66   my $record_class = get_class_from_type($type);
67   return $record_class->new(id => $id)->load;
68 }
69
70 sub new_from_workflow {
71   my ($class, $source_object, $target_type, %flags) = @_;
72
73   $flags{destination_type} = $target_type;
74   my %defaults_flags = (
75     no_linked_records => 0,
76   );
77   %flags = (%defaults_flags, %flags);
78
79   my $target_class = get_class_from_type($target_type);
80   my $target_object = ${target_class}->new_from($source_object, %flags);
81   return $target_object;
82 }
83
84 sub new_from_workflow_multi {
85   my ($class, $source_objects, $target_type, %flags) = @_;
86
87   my $target_class = get_class_from_type($target_type);
88   my $target_object = ${target_class}->new_from_multi($source_objects, %flags);
89
90   return $target_object;
91 }
92
93 sub increment_subversion {
94   my ($class, $record, %flags) = @_;
95
96   if ($record->type_data->features('subversions')) {
97     $record->increment_version_number;
98   } else {
99     die t8('Subversions are not supported or disabled for this record type.');
100   }
101
102   return;
103 }
104
105 sub get_best_price_and_discount_source {
106   my ($class, $record, $item, %flags) = @_;
107
108   my $ignore_given = !!$flags{ignore_given};
109
110   my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
111
112   my $price_src;
113   if ( $item->part->is_assortment ) {
114     # add assortment items with price 0, as the components carry the price
115     $price_src = $price_source->price_from_source("");
116     $price_src->price(0);
117   } elsif (!$ignore_given && defined $item->sellprice) {
118     $price_src = $price_source->price_from_source("");
119     $price_src->price($item->sellprice);
120   } else {
121     $price_src = $price_source->best_price
122                ? $price_source->best_price
123                : $price_source->price_from_source("");
124
125     $price_src->price($::form->round_amount($price_src->price / $record->exchangerate, 5)) if $record->can('exchangerate') && $record->exchangerate;
126     $price_src->price(0) if !$price_source->best_price;
127   }
128
129   my $discount_src;
130   if (!$ignore_given && defined $item->discount) {
131     $discount_src = $price_source->discount_from_source("");
132     $discount_src->discount($item->discount);
133   } else {
134     $discount_src = $price_source->best_discount
135                   ? $price_source->best_discount
136                   : $price_source->discount_from_source("");
137     $discount_src->discount(0) if !$price_source->best_discount;
138   }
139
140   return ($price_src, $discount_src);
141 }
142
143 sub delete {
144   my ($class, $record, %flags) = @_;
145
146   my $errors = [];
147   my $db = $record->db;
148
149   $db->with_transaction(
150     sub {
151       my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $record->id ]) };
152       $record->delete;
153       my $spool = $::lx_office_conf{paths}->{spool};
154       unlink map { "$spool/$_" } @spoolfiles if $spool;
155
156       _save_history($record,'DELETED');
157
158       1;
159   }) || push(@{$errors}, $db->error);
160
161   die t8("Errors while deleting record:") . "\n" . join("\n", @{$errors}) . "\n" if scalar @{$errors};
162 }
163
164 sub _get_history_snumbers {
165   my ($record) = @_;
166
167   my $number_type = $record->type_data->properties( 'nr_key');
168   my $snumbers    = $number_type . '_' . $record->$number_type;
169
170   return $snumbers;
171 }
172
173 sub _save_history {
174   my ($record, $addition) = @_;
175
176   SL::DB::History->new(
177     trans_id    => $record->id,
178     employee_id => SL::DB::Manager::Employee->current->id,
179     what_done   => $record->type,
180     snumbers    => _get_history_snumbers($record),
181     addition    => $addition,
182   )->save;
183 }
184
185 sub save {
186   my ($class, $record, %params) = @_;
187
188   # Test for no items
189   if (scalar @{$record->items} == 0
190       && !grep { $record->record_type eq $_ }
191          @{$::instance_conf->get_allowed_documents_with_no_positions() || []}) {
192     die t8('The action you\'ve chosen has not been executed because the document does not contain any item yet.');
193   }
194
195   $record->calculate_prices_and_taxes() if $record->type_data->features('price_tax');
196
197   foreach my $item (@{ $record->items }) {
198     # autovivify all cvars that are not in the form (cvars_by_config can do it).
199     # workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
200     foreach my $var (@{ $item->cvars_by_config }) {
201       $var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
202     }
203     $item->parse_custom_variable_values;
204   }
205
206   SL::DB->client->with_transaction(sub {
207     # validity token
208     my $validity_token;
209     if (my $validity_token_specs = $params{with_validity_token}) {
210       if (!defined $validity_token_specs->{scope} || !exists $validity_token_specs->{token}) {
211         croak ('you must provide a hash ref "with_validity_token" with the keys "scope" and "token" if you want the token to be handled');
212       }
213
214       if (!$record->id) {
215         $validity_token = SL::DB::Manager::ValidityToken->fetch_valid_token(
216           scope => $validity_token_specs->{scope},
217           token => $validity_token_specs->{token},
218         );
219
220         die $::locale->text('The form is not valid anymore.') if !$validity_token;
221       }
222     }
223
224     # delete custom shipto if it is to be deleted or if it is empty
225     if ($params{delete_custom_shipto}) { # flag?
226       if ($record->custom_shipto) {
227         $record->custom_shipto->delete if $record->custom_shipto->shipto_id;
228         $record->custom_shipto(undef);
229       }
230     }
231
232     $_->delete for @{ $params{items_to_delete} || [] };
233
234     $record->save(cascade => 1);
235
236     if ($params{objects_to_close} && @{$params{objects_to_close}}) {
237       $_->update_attributes(closed => 1) for @{$params{objects_to_close}};
238     }
239
240     # link records for requirement specs
241     if (my $converted_from_ids = $params{link_requirement_specs_linking_to_created_from_objects}) {
242       _link_requirement_specs_linking_to_created_from_objects($record, $converted_from_ids);
243     }
244
245     if ($params{set_project_in_linked_requirement_specs}) { # flag?
246       _set_project_in_linked_requirement_specs($record);
247     }
248
249     _save_history($record, 'SAVED');
250
251     $validity_token->delete if $validity_token;
252
253     1;
254   }) or die t8('Saving the record failed: #1', SL::DB->client->error);
255 }
256
257 # Todo: put this into SL::DB::Order?
258 sub _link_requirement_specs_linking_to_created_from_objects {
259   my ($record, $converted_from_oe_ids) = @_;
260
261   return unless  $converted_from_oe_ids;
262   return unless @$converted_from_oe_ids;
263
264   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $converted_from_oe_ids ]);
265   foreach my $rs_order (@{ $rs_orders }) {
266     SL::DB::RequirementSpecOrder->new(
267       order_id            => $record->id,
268       requirement_spec_id => $rs_order->requirement_spec_id,
269       version_id          => $rs_order->version_id,
270     )->save;
271   }
272 }
273
274 sub _set_project_in_linked_requirement_specs {
275   my ($record) = @_;
276
277   return unless $record->globalproject_id;
278
279   my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $record->id ]);
280   foreach my $rs_order (@{ $rs_orders }) {
281     next if $rs_order->requirement_spec->project_id == $record->globalproject_id;
282
283     $rs_order->requirement_spec->update_attributes(project_id => $record->globalproject_id);
284   }
285 }
286
287 sub clone_for_save_as_new {
288   my ($class, $saved_record, $changed_record, %params) = @_;
289
290   # changed_record
291   my %new_attrs;
292   # Lets assign a new number if the user hasn't changed the previous one.
293   # If it has been changed manually then use it as-is.
294   $new_attrs{record_number}    = (trim($changed_record->record_number) eq $saved_record->record_number)
295                         ? ''
296                         : trim($changed_record->record_number);
297
298   # Clear transdate unless changed
299   $new_attrs{transdate} = ($changed_record->transdate == $saved_record->transdate)
300                         ? DateTime->today_local
301                         : $changed_record->transdate;
302
303   # Set new reqdate unless changed if it is enabled in client config
304   if ($changed_record->reqdate == $saved_record->reqdate) {
305       $new_attrs{reqdate} = $changed_record->type_data->defaults('reqdate');
306   }
307
308   # Update employee
309   $new_attrs{employee}  = SL::DB::Manager::Employee->current;
310
311
312   my $new_record = SL::Model::Record->new_from_workflow($changed_record, $saved_record->type, no_linked_records => 1, attributes => \%new_attrs);
313
314   return $new_record;
315 }
316
317
318 1;
319
320 __END__
321
322 =encoding utf-8
323
324 =head1 NAME
325
326 SL::Model::Record - shared computations for orders (Order), delivery orders (DeliveryOrder), invoices (Invoice) and reclamations (Reclamation)
327
328 =head1 DESCRIPTION
329
330 This module contains shared behaviour among the main record object types. A given record needs to be already parsed into a Rose object.
331 All records are treated agnostically and the underlying class needs to implement a type_data call to query for differing behaviour.
332
333 Currently the following classes and types are supported:
334
335 =over 4
336
337 =item * L<SL::DB::Order>
338
339 =over 4
340
341 =item * C<sales_order>
342
343 =item * C<purchase_order>
344
345 =item * C<sales_quotation>
346
347 =item * C<purchase_quotation>
348
349 =item * C<purchase_quotation_intake>
350
351 =item * C<sales_order_intake>
352
353 =back
354
355 =item * L<SL::DB::DeliveryOrder>
356
357 =over 4
358
359 =item * C<sales_delivery_order>
360
361 =item * C<purchase_delivery_order>
362
363 =item * C<supplier_delivery_order>
364
365 =item * C<rma_delivery_order>
366
367 =back
368
369 =item * L<SL::DB::Reclamation>
370
371 =over 4
372
373 =item * C<sales_reclamation>
374
375 =item * C<purchase_reclamation>
376
377 =back
378
379 =back
380
381 The base record types need to implement a type_data call that can be queried
382 for various type informations.
383
384      +-------+              type_data()      +-------------------------+
385      | Order | ---------------proxy------->  | SL::DB::Order::TypeData |
386      +-------+                               +-------------------------+
387
388      +---------------+      type_data()      +---------------------------------+
389      | DeliveryOrder |  ------proxy------->  | SL::DB::DeliveryOrder::TypeData |
390      +---------------+                       +---------------------------------+
391
392      ...
393
394 Any Record that implements the necessary type_data callbacks can be used as a
395 record in here .
396
397 Invoices are not supported as of now, but are planned for the future.
398
399 The old delivery order C<sales_delivery_order> and C<purchase_delivery_order>
400 must be implemented in the new DeliveryOrder Controller
401
402 =head1 METHODS
403
404 =over 4
405
406 =item C<update_after_new>
407
408 Updates a record_object corresponding to type_data.
409 Sets reqdate and transdate.
410
411 Returns the record object.
412
413 =item C<update_after_customer_vendor_change>
414
415 Updates a record_object corresponding to customer/vendor and type_data.
416 Sets taxzone_id, payment_id, delivery_term_id, currency_id, language_id and
417 intnotes to customer/vendor. For sales records salesman and taxincluded is set.
418 Also for sales record with the feature 'price_tax' the billing address is updated.
419
420 Returns the record object.
421
422 =item C<new_from_workflow>
423
424 Expects source_object, target_type and can have flags.
425 Creates a new record from a by target_class->new_from(source_record).
426 Set default flag no_link_record to false.
427
428 Throws an error if the target_type doesn't exist.
429
430 Returns the new record object.
431
432 =item C<new_from_workflow_multi>
433
434 Expects an arrayref with source_objects, target_type and can have flags.
435 Creates a new record object from one or more source objects.
436
437 Returns the new record object.
438
439 =item C<increment_subversion>
440
441 Only for orders.
442
443 Increments the record's subversion number.
444
445 =item C<get_best_price_and_discount_source>
446
447 Get the best price and discount source for an item. You have
448 to pass the record and the item.
449
450 If the flag C<ignore_given> is not set and a price or discount already exists
451 for this item, these will be used. This means, that the price source and
452 discount source are set to empty and price of the price source is set to
453 the existing price and/or the discount of the discount source is set to
454 the existing discount.
455
456 If the flag C<ignore_given> is set, the best price and discount source
457 is determined via C<SL::PriceSource> and a given price or discount in the
458 item will be ignored. This can be used to get an default price/discount
459 that can be displayed to the user even if a price/discount is already
460 entered.
461
462 Returns an reference to an array where the first element is the best
463 price source and the second element is the best discount source.
464
465 =item C<delete>
466
467 Expects a record to delete.
468 Deletes the whole record and puts an entry in the history.
469 Cleans up the spool directory.
470 Dies and throws an error if there is a dberror.
471
472 TODO: check status order once old deliveryorder (do) is implemented.
473
474 =item C<save>
475
476 Expects a record to be saved and params to handle stuff like validity_token, custom_shipto,
477 items_to_delete, close objects and requirement_specs.
478
479 =over 2
480
481 =item * L<params:>
482
483 =over 4
484
485 =item * C<with_validity_token → scope>
486
487 =item * C<delete custom shipto if empty>
488
489 =item * C<items_to_delete>
490
491 =item * C<objects_to_close>
492
493 =item * C<link_requirement_specs_linking_to_created_from_objects>
494
495 =item * C<set_project_in_linked_requirement_specs>
496
497 =back
498
499 Sets an entry in the history.
500
501 Dies and throws an error when there is an error.
502
503 =back
504
505 =back
506
507 =over 4
508
509 =item C<clone_for_save_as_new>
510
511 Expects the saved record and the record to be changed.
512
513 Sets the actual employee.
514
515 Also sets a new transdate, new reqdate and an empty recordnumber if it wasn't already changed in the old record.
516
517 =item C<_save_history>
518
519 Expects a record and an addition reason for the history (SAVED,DELETED,...)
520
521 =item C<_get_history_snumbers>
522
523 Expects a record, returns snumber for the history entry.
524
525 =back
526
527 =head1 BUGS
528
529 None yet. :)
530
531 =head1 FURTHER WORK
532
533 =over 4
534
535 =item *
536
537 Handling of price sources and prices in controllers
538
539 =item *
540
541 Handling of shippedqty calculations in controllers
542
543 =item *
544
545 Autovivification of unparsed cvar configs is still in parsing code
546
547 =item *
548
549 sellprice changed handling
550
551 =back
552
553
554 The traits currently encoded in the type data classes should also be extended to cover:
555
556 =over 4
557
558 =item *
559
560 PeriodicInvoices
561
562 =item *
563
564 Exchangerates
565
566 =item *
567
568 Payments for invoices
569
570 =back
571
572 In later stages the following things should be implemented:
573
574 =over 4
575
576 =item *
577
578 Further encapsulate the linking logic for creating linked records.
579
580 =item *
581
582 Better tests for auto-close of quotations and auto-delivered of delivery orders on save. Best to move those into post-save hooks as well.
583
584 =item *
585
586 More tests of workflow related conversions from frontend (current tests are mostly at the SL::Model::Record boundary).
587
588 =item *
589
590 More tests for error handling in controllers. I.e. if the given recordnumber is kept.
591
592 =back
593
594 =head1 AUTHORS
595
596 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
597
598 Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
599
600 Werner Hahn E<lt>wh@futureworldsearch.netE<gt>
601
602 ...
603
604 =cut