Project

General

Profile

Download (13.9 KB) Statistics
| Branch: | Tag: | Revision:
package SL::Model::Record;

use strict;

use Carp;

use SL::DB::Employee;
use SL::DB::Order;
use SL::DB::DeliveryOrder;
use SL::DB::Reclamation;
use SL::DB::RequirementSpecOrder;
use SL::DB::History;
use SL::DB::Invoice;
use SL::DB::Status;
use SL::DB::ValidityToken;
use SL::DB::Order::TypeData qw(:types);
use SL::DB::DeliveryOrder::TypeData qw(:types);
use SL::DB::Reclamation::TypeData qw(:types);
use SL::DB::Helper::Record qw(get_class_from_type);

use SL::Util qw(trim);
use SL::Locale::String qw(t8);


sub update_after_new {
my ($class, $new_record, %flags) = @_;

$new_record->transdate(DateTime->now_local());

my $default_reqdate = $new_record->type_data->defaults('reqdate');
$new_record->reqdate($default_reqdate);

return $new_record;
}

sub update_after_customer_vendor_change {
my ($class, $record) = @_;
my $new_customervendor = $record->customervendor;

$record->$_($new_customervendor->$_) for (qw(
taxzone_id payment_id delivery_term_id currency_id language_id
));

$record->intnotes($new_customervendor->notes);

return if !$record->is_sales;
if ($record->is_sales) {
my $new_customer = $new_customervendor;
$record->salesman_id($new_customer->salesman_id
|| SL::DB::Manager::Employee->current->id);
$record->taxincluded(defined($new_customer->taxincluded_checked)
? $new_customer->taxincluded_checked
: $::myconfig{taxincluded_checked});
if ($record->type_data->features('price_tax')) {
my $address = $new_customer->default_billing_address;;
$record->billing_address_id($address ? $address->id : undef);
}
}

return $record;
}

sub get_record {
my ($class, $type, $id) = @_;
my $record_class = get_class_from_type($type);
return $record_class->new(id => $id)->load;
}

sub new_from_workflow {
my ($class, $source_object, $target_type, %flags) = @_;

$flags{destination_type} = $target_type;
my %defaults_flags = (
no_linked_records => 0,
);
%flags = (%defaults_flags, %flags);

my $target_class = get_class_from_type($target_type);
my $target_object = ${target_class}->new_from($source_object, %flags);
return $target_object;
}

sub new_from_workflow_multi {
my ($class, $source_objects, $target_type, %flags) = @_;

my $target_class = get_class_from_type($target_type);
my $target_object = ${target_class}->new_from_multi($source_objects, %flags);

return $target_object;
}

sub increment_subversion {
my ($class, $record, %flags) = @_;

$record->increment_version_number if $record->type_data->features('subversions');

return;
}

sub delete {
my ($class, $record, %flags) = @_;

my $errors = [];
my $db = $record->db;

$db->with_transaction(
sub {
my @spoolfiles = grep { $_ } map { $_->spoolfile } @{ SL::DB::Manager::Status->get_all(where => [ trans_id => $record->id ]) };
$record->delete;
my $spool = $::lx_office_conf{paths}->{spool};
unlink map { "$spool/$_" } @spoolfiles if $spool;

_save_history($record,'DELETED');

1;
}) || push(@{$errors}, $db->error);

die t8("Errors while deleting record:") . "\n" . join("\n", @{$errors}) . "\n" if scalar @{$errors};
}

sub _get_history_snumbers {
my ($record) = @_;

my $number_type = $record->type_data->properties( 'nr_key');
my $snumbers = $number_type . '_' . $record->$number_type;

return $snumbers;
}

sub _save_history {
my ($record, $addition) = @_;

SL::DB::History->new(
trans_id => $record->id,
employee_id => SL::DB::Manager::Employee->current->id,
what_done => $record->type,
snumbers => _get_history_snumbers($record),
addition => $addition,
)->save;
}

sub save {
my ($class, $record, %params) = @_;

# Test for no items
if (scalar @{$record->items} == 0
&& !grep { $record->record_type eq $_ }
@{$::instance_conf->get_allowed_documents_with_no_positions() || []}) {
die t8('The action you\'ve chosen has not been executed because the document does not contain any item yet.');
}

$record->calculate_prices_and_taxes() if $record->type_data->features('price_tax');

foreach my $item (@{ $record->items }) {
# autovivify all cvars that are not in the form (cvars_by_config can do it).
# workaround to pre-parse number-cvars (parse_custom_variable_values does not parse number values).
foreach my $var (@{ $item->cvars_by_config }) {
$var->unparsed_value($::form->parse_amount(\%::myconfig, $var->{__unparsed_value})) if ($var->config->type eq 'number' && exists($var->{__unparsed_value}));
}
$item->parse_custom_variable_values;
}

SL::DB->client->with_transaction(sub {
# validity token
my $validity_token;
if (my $validity_token_specs = $params{with_validity_token}) {
if (!defined $validity_token_specs->{scope} || !exists $validity_token_specs->{token}) {
croak ('you must provide a hash ref "with_validity_token" with the keys "scope" and "token" if you want the token to be handled');
}

if (!$record->id) {
$validity_token = SL::DB::Manager::ValidityToken->fetch_valid_token(
scope => $validity_token_specs->{scope},
token => $validity_token_specs->{token},
);

die $::locale->text('The form is not valid anymore.') if !$validity_token;
}
}

# delete custom shipto if it is to be deleted or if it is empty
if ($params{delete_custom_shipto}) { # flag?
if ($record->custom_shipto) {
$record->custom_shipto->delete if $record->custom_shipto->shipto_id;
$record->custom_shipto(undef);
}
}

$_->delete for @{ $params{items_to_delete} || [] };

$record->save(cascade => 1);

if ($params{objects_to_close} && @{$params{objects_to_close}}) {
$_->update_attributes(closed => 1) for @{$params{objects_to_close}};
}

# link records for requirement specs
if (my $converted_from_ids = $params{link_requirement_specs_linking_to_created_from_objects}) {
_link_requirement_specs_linking_to_created_from_objects($record, $converted_from_ids);
}

if ($params{set_project_in_linked_requirement_specs}) { # flag?
_set_project_in_linked_requirement_specs($record);
}

_save_history($record, 'SAVED');

$validity_token->delete if $validity_token;

1;
}) or die t8('Saving the record failed: #1', SL::DB->client->error);
}

# Todo: put this into SL::DB::Order?
sub _link_requirement_specs_linking_to_created_from_objects {
my ($record, $converted_from_oe_ids) = @_;

return unless $converted_from_oe_ids;
return unless @$converted_from_oe_ids;

my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $converted_from_oe_ids ]);
foreach my $rs_order (@{ $rs_orders }) {
SL::DB::RequirementSpecOrder->new(
order_id => $record->id,
requirement_spec_id => $rs_order->requirement_spec_id,
version_id => $rs_order->version_id,
)->save;
}
}

sub _set_project_in_linked_requirement_specs {
my ($record) = @_;

return unless $record->globalproject_id;

my $rs_orders = SL::DB::Manager::RequirementSpecOrder->get_all(where => [ order_id => $record->id ]);
foreach my $rs_order (@{ $rs_orders }) {
next if $rs_order->requirement_spec->project_id == $record->globalproject_id;

$rs_order->requirement_spec->update_attributes(project_id => $record->globalproject_id);
}
}

sub clone_for_save_as_new {
my ($class, $saved_record, $changed_record, %params) = @_;

# changed_record
my %new_attrs;
# Lets assign a new number if the user hasn't changed the previous one.
# If it has been changed manually then use it as-is.
$new_attrs{record_number} = (trim($changed_record->record_number) eq $saved_record->record_number)
? ''
: trim($changed_record->record_number);

# Clear transdate unless changed
$new_attrs{transdate} = ($changed_record->transdate == $saved_record->transdate)
? DateTime->today_local
: $changed_record->transdate;

# Set new reqdate unless changed if it is enabled in client config
if ($changed_record->reqdate == $saved_record->reqdate) {
$new_attrs{reqdate} = $changed_record->type_data->defaults('reqdate');
}

# Update employee
$new_attrs{employee} = SL::DB::Manager::Employee->current;


my $new_record = SL::Model::Record->new_from_workflow($changed_record, $saved_record->type, no_linked_records => 1, attributes => \%new_attrs);

return $new_record;
}


1;

__END__

=encoding utf-8

=head1 NAME

SL::Model::Record - shared computations for orders (Order), delivery orders (DeliveryOrder), invoices (Invoice) and reclamations (Reclamation)

=head1 DESCRIPTION

This module contains shared behaviour among the main record object types. A given record needs to be already parsed into a Rose object.
All records are treated agnostically and the underlying class needs to implement a type_data call to query for differing behaviour.

Currently the following classes and types are supported:

=over 4

=item * L<SL::DB::Order>

=over 4

=item * C<sales_order>

=item * C<purchase_order>

=item * C<sales_quotation>

=item * C<purchase_quotation>

=item * C<purchase_quotation_intake>

=item * C<sales_order_intake>

=back

=item * L<SL::DB::DeliveryOrder>

=over 4

=item * C<sales_delivery_order>

=item * C<purchase_delivery_order>

=item * C<supplier_delivery_order>

=item * C<rma_delivery_order>

=back

=item * L<SL::DB::Reclamation>

=over 4

=item * C<sales_reclamation>

=item * C<purchase_reclamation>

=back

=back

The base record types need to implement a type_data call that can be queried
for various type informations.

+-------+ type_data() +-------------------------+
| Order | ---------------proxy-------> | SL::DB::Order::TypeData |
+-------+ +-------------------------+

+---------------+ type_data() +---------------------------------+
| DeliveryOrder | ------proxy-------> | SL::DB::DeliveryOrder::TypeData |
+---------------+ +---------------------------------+

...

Any Record that implements the necessary type_data callbacks can be used as a
record in here .

Invoices are not supported as of now, but are planned for the future.

The old delivery order C<sales_delivery_order> and C<purchase_delivery_order>
must be implemented in the new DeliveryOrder Controller

=head1 METHODS

=over 4

=item C<update_after_new>

Updates a record_object corresponding to type_data.
Sets reqdate and transdate.

Returns the record object.

=item C<update_after_customer_vendor_change>

Updates a record_object corresponding to customer/vendor and type_data.
Sets taxzone_id, payment_id, delivery_term_id, currency_id, language_id and
intnotes to customer/vendor. For sales records salesman and taxincluded is set.
Also for sales record with the feature 'price_tax' the billing address is updated.

Returns the record object.

=item C<new_from_workflow>

Expects source_object, target_type and can have flags.
Creates a new record from a by target_class->new_from(source_record).
Set default flag no_link_record to false.

Throws an error if the target_type doesn't exist.

Returns the new record object.

=item C<new_from_workflow_multi>

Expects an arrayref with source_objects, target_type and can have flags.
Creates a new record object from one or more source objects.

Returns the new record object.

=item C<increment_subversion>

Only for orders.

Increments the record's subversion number.

TODO: check type data if this is allowed/supported for this record and trow exception or error

=item C<delete>

Expects a record to delete.
Deletes the whole record and puts an entry in the history.
Cleans up the spool directory.
Dies and throws an error if there is a dberror.

TODO: check status order once old deliveryorder (do) is implemented.

=item C<save>

Expects a record to be saved and params to handle stuff like validity_token, custom_shipto,
items_to_delete, close objects and requirement_specs.

=over 2

=item * L<params:>

=over 4

=item * C<with_validity_token → scope>

=item * C<delete custom shipto if empty>

=item * C<items_to_delete>

=item * C<objects_to_close>

=item * C<link_requirement_specs_linking_to_created_from_objects>

=item * C<set_project_in_linked_requirement_specs>

=back

Sets an entry in the history.

Dies and throws an error when there is an error.

=back

=back

=over 4

=item C<clone_for_save_as_new>

Expects the saved record and the record to be changed.

Sets the actual employee.

Also sets a new transdate, new reqdate and an empty recordnumber if it wasn't already changed in the old record.

=item C<_save_history>

Expects a record and an addition reason for the history (SAVED,DELETED,...)

=item C<_get_history_snumbers>

Expects a record, returns snumber for the history entry.

=back

=head1 BUGS

None yet. :)

=head1 FURTHER WORK

=over 4

=item *

Handling of price sources and prices in controllers

=item *

Handling of shippedqty calculations in controllers

=item *

Autovivification of unparsed cvar configs is still in parsing code

=item *

sellprice changed handling

=back


The traits currently encoded in the type data classes should also be extended to cover:

=over 4

=item *

PeriodicInvoices

=item *

Exchangerates

=item *

Payments for invoices

=back

In later stages the following things should be implemented:

=over 4

=item *

Further encapsulate the linking logic for creating linked records.

=item *

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.

=item *

More tests of workflow related conversions from frontend (current tests are mostly at the SL::Model::Record boundary).

=item *

More tests for error handling in controllers. I.e. if the given recordnumber is kept.

=back

=head1 AUTHORS

Bernd Bleßmann E<lt>bernd@kivitendo-premium.deE<gt>

Tamino Steinert E<lt>tamino.steinert@tamino.stE<gt>

Werner Hahn E<lt>wh@futureworldsearch.netE<gt>

...

=cut
    (1-1/1)