1 package SL::Model::Record;
9 use SL::DB::DeliveryOrder;
10 use SL::DB::Reclamation;
11 use SL::DB::RequirementSpecOrder;
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);
21 use SL::Util qw(trim);
22 use SL::Locale::String qw(t8);
26 sub update_after_new {
27 my ($class, $new_record, %flags) = @_;
29 $new_record->transdate(DateTime->now_local());
31 my $default_reqdate = $new_record->type_data->defaults('reqdate');
32 $new_record->reqdate($default_reqdate);
37 sub update_after_customer_vendor_change {
38 my ($class, $record) = @_;
39 my $new_customervendor = $record->customervendor;
41 $record->$_($new_customervendor->$_) for (qw(
42 taxzone_id payment_id delivery_term_id currency_id language_id
45 $record->intnotes($new_customervendor->notes);
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);
65 my ($class, $type, $id) = @_;
66 my $record_class = get_class_from_type($type);
67 return $record_class->new(id => $id)->load;
70 sub new_from_workflow {
71 my ($class, $source_object, $target_type, %flags) = @_;
73 $flags{destination_type} = $target_type;
74 my %defaults_flags = (
75 no_linked_records => 0,
77 %flags = (%defaults_flags, %flags);
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;
84 sub new_from_workflow_multi {
85 my ($class, $source_objects, $target_type, %flags) = @_;
87 my $target_class = get_class_from_type($target_type);
88 my $target_object = ${target_class}->new_from_multi($source_objects, %flags);
90 return $target_object;
93 sub increment_subversion {
94 my ($class, $record, %flags) = @_;
96 if ($record->type_data->features('subversions')) {
97 $record->increment_version_number;
99 die t8('Subversions are not supported or disabled for this record type.');
105 sub get_best_price_and_discount_source {
106 my ($class, $record, $item, %flags) = @_;
108 my $ignore_given = !!$flags{ignore_given};
110 my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
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);
121 $price_src = $price_source->best_price
122 ? $price_source->best_price
123 : $price_source->price_from_source("");
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;
130 if (!$ignore_given && defined $item->discount) {
131 $discount_src = $price_source->discount_from_source("");
132 $discount_src->discount($item->discount);
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;
140 return ($price_src, $discount_src);
144 my ($class, $record, %flags) = @_;
147 my $db = $record->db;
149 $db->with_transaction(
151 my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $record->id ]) };
153 my $spool = $::lx_office_conf{paths}->{spool};
154 unlink map { "$spool/$_" } @spoolfiles if $spool;
156 _save_history($record,'DELETED');
159 }) || push(@{$errors}, $db->error);
161 die t8("Errors while deleting record:") . "\n" . join("\n", @{$errors}) . "\n" if scalar @{$errors};
164 sub _get_history_snumbers {
167 my $number_type = $record->type_data->properties( 'nr_key');
168 my $snumbers = $number_type . '_' . $record->$number_type;
174 my ($record, $addition) = @_;
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,
186 my ($class, $record, %params) = @_;
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.');
195 $record->calculate_prices_and_taxes() if $record->type_data->features('price_tax');
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}));
203 $item->parse_custom_variable_values;
206 SL::DB->client->with_transaction(sub {
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');
215 $validity_token = SL::DB::Manager::ValidityToken->fetch_valid_token(
216 scope => $validity_token_specs->{scope},
217 token => $validity_token_specs->{token},
220 die $::locale->text('The form is not valid anymore.') if !$validity_token;
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);
232 $_->delete for @{ $params{items_to_delete} || [] };
234 $record->save(cascade => 1);
236 if ($params{objects_to_close} && @{$params{objects_to_close}}) {
237 $_->update_attributes(closed => 1) for @{$params{objects_to_close}};
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);
245 if ($params{set_project_in_linked_requirement_specs}) { # flag?
246 _set_project_in_linked_requirement_specs($record);
249 _save_history($record, 'SAVED');
251 $validity_token->delete if $validity_token;
254 }) or die t8('Saving the record failed: #1', SL::DB->client->error);
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) = @_;
261 return unless $converted_from_oe_ids;
262 return unless @$converted_from_oe_ids;
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,
274 sub _set_project_in_linked_requirement_specs {
277 return unless $record->globalproject_id;
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;
283 $rs_order->requirement_spec->update_attributes(project_id => $record->globalproject_id);
287 sub clone_for_save_as_new {
288 my ($class, $saved_record, $changed_record, %params) = @_;
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)
296 : trim($changed_record->record_number);
298 # Clear transdate unless changed
299 $new_attrs{transdate} = ($changed_record->transdate == $saved_record->transdate)
300 ? DateTime->today_local
301 : $changed_record->transdate;
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');
309 $new_attrs{employee} = SL::DB::Manager::Employee->current;
312 my $new_record = SL::Model::Record->new_from_workflow($changed_record, $saved_record->type, no_linked_records => 1, attributes => \%new_attrs);
326 SL::Model::Record - shared computations for orders (Order), delivery orders (DeliveryOrder), invoices (Invoice) and reclamations (Reclamation)
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.
333 Currently the following classes and types are supported:
337 =item * L<SL::DB::Order>
341 =item * C<sales_order>
343 =item * C<purchase_order>
345 =item * C<sales_quotation>
347 =item * C<purchase_quotation>
349 =item * C<purchase_quotation_intake>
351 =item * C<sales_order_intake>
355 =item * L<SL::DB::DeliveryOrder>
359 =item * C<sales_delivery_order>
361 =item * C<purchase_delivery_order>
363 =item * C<supplier_delivery_order>
365 =item * C<rma_delivery_order>
369 =item * L<SL::DB::Reclamation>
373 =item * C<sales_reclamation>
375 =item * C<purchase_reclamation>
381 The base record types need to implement a type_data call that can be queried
382 for various type informations.
384 +-------+ type_data() +-------------------------+
385 | Order | ---------------proxy-------> | SL::DB::Order::TypeData |
386 +-------+ +-------------------------+
388 +---------------+ type_data() +---------------------------------+
389 | DeliveryOrder | ------proxy-------> | SL::DB::DeliveryOrder::TypeData |
390 +---------------+ +---------------------------------+
394 Any Record that implements the necessary type_data callbacks can be used as a
397 Invoices are not supported as of now, but are planned for the future.
399 The old delivery order C<sales_delivery_order> and C<purchase_delivery_order>
400 must be implemented in the new DeliveryOrder Controller
406 =item C<update_after_new>
408 Updates a record_object corresponding to type_data.
409 Sets reqdate and transdate.
411 Returns the record object.
413 =item C<update_after_customer_vendor_change>
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.
420 Returns the record object.
422 =item C<new_from_workflow>
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.
428 Throws an error if the target_type doesn't exist.
430 Returns the new record object.
432 =item C<new_from_workflow_multi>
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.
437 Returns the new record object.
439 =item C<increment_subversion>
443 Increments the record's subversion number.
445 =item C<get_best_price_and_discount_source>
447 Get the best price and discount source for an item. You have
448 to pass the record and the item.
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.
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
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.
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.
472 TODO: check status order once old deliveryorder (do) is implemented.
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.
485 =item * C<with_validity_token → scope>
487 =item * C<delete custom shipto if empty>
489 =item * C<items_to_delete>
491 =item * C<objects_to_close>
493 =item * C<link_requirement_specs_linking_to_created_from_objects>
495 =item * C<set_project_in_linked_requirement_specs>
499 Sets an entry in the history.
501 Dies and throws an error when there is an error.
509 =item C<clone_for_save_as_new>
511 Expects the saved record and the record to be changed.
513 Sets the actual employee.
515 Also sets a new transdate, new reqdate and an empty recordnumber if it wasn't already changed in the old record.
517 =item C<_save_history>
519 Expects a record and an addition reason for the history (SAVED,DELETED,...)
521 =item C<_get_history_snumbers>
523 Expects a record, returns snumber for the history entry.
537 Handling of price sources and prices in controllers
541 Handling of shippedqty calculations in controllers
545 Autovivification of unparsed cvar configs is still in parsing code
549 sellprice changed handling
554 The traits currently encoded in the type data classes should also be extended to cover:
568 Payments for invoices
572 In later stages the following things should be implemented:
578 Further encapsulate the linking logic for creating linked records.
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.
586 More tests of workflow related conversions from frontend (current tests are mostly at the SL::Model::Record boundary).
590 More tests for error handling in controllers. I.e. if the given recordnumber is kept.
596 Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>
598 Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>
600 Werner Hahn E<lt>wh@futureworldsearch.netE<gt>