module => 'IC',
cvars_alias => 1,
);
+use List::Util qw(sum);
__PACKAGE__->meta->add_relationships(
assemblies => {
type => 'one to many',
class => 'SL::DB::Assembly',
+ manager_args => { sort_by => 'position, oid' },
column_map => { id => 'id' },
},
prices => {
makemodels => {
type => 'one to many',
class => 'SL::DB::MakeModel',
+ manager_args => { sort_by => 'sortorder' },
column_map => { id => 'parts_id' },
},
translations => {
class => 'SL::DB::AssortmentItem',
column_map => { id => 'assortment_id' },
},
+ history_entries => {
+ type => 'one to many',
+ class => 'SL::DB::History',
+ column_map => { id => 'trans_id' },
+ query_args => [ what_done => 'part' ],
+ manager_args => { sort_by => 'itime' },
+ },
);
__PACKAGE__->meta->initialize;
return 1;
}
+sub items {
+ my ($self) = @_;
+
+ if ( $self->part_type eq 'assembly' ) {
+ return $self->assemblies;
+ } elsif ( $self->part_type eq 'assortment' ) {
+ return $self->assortment_items;
+ } else {
+ return undef;
+ }
+}
+
+sub items_checksum {
+ my ($self) = @_;
+
+ # for detecting if the items of an (orphaned) assembly or assortment have
+ # changed when saving
+
+ return join(' ', sort map { $_->part->id } @{$self->items});
+};
+
+sub validate {
+ my ($self) = @_;
+
+ my @errors;
+ push @errors, $::locale->text('The partnumber is missing.') if $self->id and !$self->partnumber;
+ push @errors, $::locale->text('The unit is missing.') unless $self->unit;
+ push @errors, $::locale->text('The buchungsgruppe is missing.') unless $self->buchungsgruppen_id or $self->buchungsgruppe;
+
+ unless ( $self->id ) {
+ push @errors, $::locale->text('The partnumber already exists.') if SL::DB::Manager::Part->get_all_count(where => [ partnumber => $self->partnumber ]);
+ };
+
+ if ($self->is_assortment && $self->orphaned && scalar @{$self->assortment_items} == 0) {
+ # when assortment isn't orphaned form doesn't contain any items
+ push @errors, $::locale->text('The assortment doesn\'t have any items.');
+ }
+
+ if ($self->is_assembly && scalar @{$self->assemblies} == 0) {
+ push @errors, $::locale->text('The assembly doesn\'t have any items.');
+ }
+
+ return @errors;
+}
+
sub is_type {
my $self = shift;
my $type = lc(shift || '');
- die 'invalid type' unless $type =~ /^(?:part|service|assembly)$/;
+ die 'invalid type' unless $type =~ /^(?:part|service|assembly|assortment)$/;
return $self->type eq $type ? 1 : 0;
}
-sub is_part { $_[0]->part_type eq 'part' }
-sub is_assembly { $_[0]->part_type eq 'assembly' }
-sub is_service { $_[0]->part_type eq 'service' }
+sub is_part { $_[0]->part_type eq 'part' }
+sub is_assembly { $_[0]->part_type eq 'assembly' }
+sub is_service { $_[0]->part_type eq 'service' }
+sub is_assortment { $_[0]->part_type eq 'assortment' }
sub type {
return $_[0]->part_type;
$class->new(%params, part_type => 'service');
}
+sub new_assortment {
+ my ($class, %params) = @_;
+ $class->new(%params, part_type => 'assortment');
+}
+
+sub last_modification {
+ my ($self) = @_;
+ return $self->mtime or $self->itime;
+};
+
sub orphaned {
my ($self) = @_;
die 'not an accessor' if @_ > 1;
+ return 1 unless $self->id;
+
my @relations = qw(
SL::DB::InvoiceItem
SL::DB::OrderItem
+ SL::DB::DeliveryOrderItem
SL::DB::Inventory
+ SL::DB::Assembly
+ SL::DB::AssortmentItem
);
for my $class (@relations) {
if (!exists $charts->{$taxzone}->{$type}) {
require SL::DB::Buchungsgruppe;
my $bugru = SL::DB::Buchungsgruppe->load_cached($self->buchungsgruppen_id);
- my $chart_id = ($type eq 'inventory') ? ($self->inventory_accno_id ? $bugru->inventory_accno_id : undef)
+ my $chart_id = ($type eq 'inventory') ? ($self->is_part ? $bugru->inventory_accno_id : undef)
: $bugru->call_sub("${type}_accno_id", $taxzone);
if ($chart_id) {
join ' ', grep $_, map $_[0]->$_, qw(partnumber description);
}
+sub clone_and_reset_deep {
+ my ($self) = @_;
+
+ my $clone = $self->clone_and_reset; # resets id and partnumber (primary key and unique constraint)
+ $clone->makemodels( map { $_->clone_and_reset } @{$self->makemodels} ) if @{$self->makemodels};
+ $clone->translations( map { $_->clone_and_reset } @{$self->translations} ) if @{$self->translations};
+
+ if ( $self->is_assortment ) {
+ # use clone rather than reset_and_clone because the unique constraint would also remove parts_id
+ $clone->assortment_items( map { $_->clone } @{$self->assortment_items} );
+ $_->assortment_id(undef) foreach @{ $clone->assortment_items }
+ };
+
+ if ( $self->is_assembly ) {
+ $clone->assemblies( map { $_->clone_and_reset } @{$self->assemblies});
+ };
+
+ if ( $self->prices ) {
+ $clone->prices( map { $_->clone } @{$self->prices}); # pricegroup_id gets reset here because it is part of a unique contraint
+ if ( $clone->prices ) {
+ foreach my $price ( @{$clone->prices} ) {
+ $price->id(undef);
+ $price->parts_id(undef);
+ };
+ };
+ };
+
+ return $clone;
+}
+
+sub item_diffs {
+ my ($self, $comparison_part) = @_;
+
+ die "item_diffs needs a part object" unless ref($comparison_part) eq 'SL::DB::Part';
+ die "part and comparison_part need to be of the same part_type" unless
+ ( $self->part_type eq 'assembly' or $self->part_type eq 'assortment' )
+ and ( $comparison_part->part_type eq 'assembly' or $comparison_part->part_type eq 'assortment' )
+ and $self->part_type eq $comparison_part->part_type;
+
+ # return [], [] if $self->items_checksum eq $comparison_part->items_checksum;
+ my @self_part_ids = map { $_->parts_id } $self->items;
+ my @comparison_part_ids = map { $_->parts_id } $comparison_part->items;
+
+ my %orig = map{ $_ => 1 } @self_part_ids;
+ my %comparison = map{ $_ => 1 } @comparison_part_ids;
+ my (@additions, @removals);
+ @additions = grep { !exists( $orig{$_} ) } @comparison_part_ids if @comparison_part_ids;
+ @removals = grep { !exists( $comparison{$_} ) } @self_part_ids if @self_part_ids;
+
+ return \@additions, \@removals;
+};
+
+sub items_sellprice_sum {
+ my ($self, %params) = @_;
+
+ return unless $self->is_assortment or $self->is_assembly;
+ return unless $self->items;
+
+ if ($self->is_assembly) {
+ return sum map { $_->linetotal_sellprice } @{$self->items};
+ } else {
+ return sum map { $_->linetotal_sellprice(%params) } grep { $_->charge } @{$self->items};
+ }
+}
+
+sub items_lastcost_sum {
+ my ($self) = @_;
+
+ return unless $self->is_assortment or $self->is_assembly;
+ return unless $self->items;
+ sum map { $_->linetotal_lastcost } @{$self->items};
+};
+
+sub assortment_lastcost_sum {
+ my ($self) = @_;
+
+ return unless $self->is_assortment;
+ sum map { $_->linetotal_lastcost } @{$self->assortment_items};
+};
+
1;
__END__
=item Assembly - a collection of both parts and services
+=item Assortment - a collection of items (parts or assemblies)
+
=back
These types are sadly represented by data inside the class and cannot be
migrated into a flag. To work around this, each C<Part> object knows what type
it currently is. Since the type is data driven, there ist no explicit setting
method for it, but you can construct them explicitly with C<new_part>,
-C<new_service>, and C<new_assembly>. A Buchungsgruppe should be supplied in this
+C<new_service>, C<new_assembly> and C<new_assortment>. A Buchungsgruppe should be supplied in this
case, but it will use the default Buchungsgruppe if you don't.
Matching these there are assorted helper methods dealing with types,
Please note, that this is a write only accessor, the original Buchungsgruppe can
not be retrieved from an article once set.
+=item C<assembly_sellprice_sum>
+
+Non-recursive sellprice sum of all the assembly item sellprices.
+
+=item C<assortment_sellprice_sum>
+
+Non-recursive sellprice sum of all the assortment item sellprices.
+
+=item C<assembly_lastcost_sum>
+
+Non-recursive lastcost sum of all the assembly item lastcosts.
+
+=item C<assortment_lastcost_sum>
+
+Non-recursive lastcost sum of all the assortment item lastcosts.
+
=back
=head1 AUTHORS