use strict;
use parent 'SL::DB::Object';
use Rose::Object::MakeMethods::Generic (
- scalar => [ qw(record_item record) ],
- 'array --get_set_init' => [ qw(all_price_sources) ],
+ scalar => [ qw(record_item record fast) ],
+ 'scalar --get_set_init' => [ qw(
+ best_price best_discount
+ ) ],
+ 'array --get_set_init' => [ qw(
+ all_price_sources
+ available_prices available_discounts
+ ) ],
);
use List::UtilsBy qw(min_by max_by);
my ($self) = @_;
[ map {
- $_->new(record_item => $self->record_item, record => $self->record)
+ $self->price_source_by_class($_);
} SL::PriceSource::ALL->all_enabled_price_sources ]
}
+sub price_source_by_class {
+ my ($self, $class) = @_;
+ return unless $class;
+
+ $self->{price_source_by_name}{$class} //=
+ $class->new(record_item => $self->record_item, record => $self->record, fast => $self->fast);
+}
+
sub price_from_source {
my ($self, $source) = @_;
- my ($source_name, $spec) = split m{/}, $source, 2;
+ return empty_price() if !$source;
- my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+ ${ $self->{price_from_source} //= {} }{$source} //= do {
+ my ($source_name, $spec) = split m{/}, $source, 2;
+ my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+ my $source_object = $self->price_source_by_class($class);
- return $class
- ? $class->new(record_item => $self->record_item, record => $self->record)->price_from_source($source, $spec)
- : empty_price();
+ $source_object
+ ? $source_object->price_from_source($source, $spec)
+ : empty_price();
+ }
}
sub discount_from_source {
my ($self, $source) = @_;
- my ($source_name, $spec) = split m{/}, $source, 2;
+ return empty_discount() if !$source;
- my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+ ${ $self->{discount_from_source} //= {} }{$source} //= do {
+ my ($source_name, $spec) = split m{/}, $source, 2;
+ my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
+ my $source_object = $self->price_source_by_class($class);
- return $class
- ? $class->new(record_item => $self->record_item, record => $self->record)->discount_from_source($source, $spec)
- : empty_discount();
+ $source_object
+ ? $source_object->discount_from_source($source, $spec)
+ : empty_discount();
+ }
}
-sub available_prices {
- map { $_->available_prices } $_[0]->all_price_sources;
+sub init_available_prices {
+ [ map { $_->available_prices } $_[0]->all_price_sources ];
}
-sub available_discounts {
- return if $_[0]->record_item->part->not_discountable;
- map { $_->available_discounts } $_[0]->all_price_sources;
+sub init_available_discounts {
+ return [] if $_[0]->record_item->part->not_discountable;
+ [ map { $_->available_discounts } $_[0]->all_price_sources ];
}
-sub best_price {
+sub init_best_price {
min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
}
-sub best_discount {
+sub init_best_discount {
max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
}
=head1 GUARANTEES
-To ensure price source prices are comprehensible and reproucible, some
+To ensure price source prices are comprehensible and reproducible, some
invariants are guaranteed:
=over 4
=item 2.
-If a price is set from a source, it is read only. A price edited manually is by
-definition not a sourced price.
+If a price is set from a source then the system will try to prevent the user
+from messing it up. By default this means the price will be read-only.
+Implementations can choose to make prices editable, but even then deviations
+from the calculatied price will be marked.
+
+A price that is not set from a source will not have any of this.
=item 3.
=head1 STRUCTURE
Price sources are managed by this package (L<SL::PriceSource>), and all
-external access should be by using it's interface.
+external access should be by using its interface.
Each source is an instance of L<SL::PriceSource::Base> and the available
implementations are recorded in L<SL::PriceSource::ALL>. Prices and discounts
returned by interface methods are instances of L<SL::PriceSource::Price> and
-L<SL::PriceSource::Discout>.
+L<SL::PriceSource::Discount>.
Returned prices and discounts should be checked for entries in C<invalid> and
C<missing>, see documentation in their classes.
=item C<best_price>
-Attempts to get the best available price. returns L<empty_price> if no price is found.
+Attempts to get the best available price. returns L<empty_price> if no price is
+found.
=item C<best_discount>
-Attempts to get the best available discount. returns L<empty_discount> if no discount is found.
+Attempts to get the best available discount. returns L<empty_discount> if no
+discount is found.
=item C<empty_price>
-A special empty price, that does not change the previously entered price, and
+A special empty price that does not change the previously entered price and
opens the price field to manual changes.
=item C<empty_discount>
-A special empty discount, that does not change the previously entered discount, and
-opens the discount field to manual changes.
+A special empty discount that does not change the previously entered discount
+and opens the discount field to manual changes.
+
+=item C<fast>
+
+If set to true, indicates that calls may skip doing intensive work and instead
+return a price or discount flagged as unknown. The caller must be prepared to
+deal with those.
+
+Typically this is intended to delay expensive calculations until they can be
+done in a second batch pass. If the information is already present, it is still
+encouraged that implementations return the correct values.
=back
+
=head1 SEE ALSO
L<SL::PriceSource::Base>,
A possible solution is to either split price sources into simple and complex
ones (where the former do not require records).
-Another would be to have default values for the inpout normally taken from
+Another would be to have default values for the input normally taken from
records (like qty defaulting to 1).
A last one would be to provide an alternative input channel for needed
=item *
Discount sources were implemented as a copy of the prices with slightly
-different semantics. Need to do a real design. A requirement is, that a sinle
+different semantics. Need to do a real design. A requirement is, that a single
source can provide both prices and discounts (needed for price_rules).
=item *
=item *
-Composing price sources are disallowed for clarity, but all price sources need
-to be aware of units. This is madness.
+Composing price sources is disallowed for clarity, but all price sources need
+to be aware of units and price_factors. This is madness.
+
+=item *
+
+The current implementation of lastcost is useless. Since it's one of the
+master_data prices it will always compete with listprice. But in real scenarios
+the listprice tends to go up, while lastcost stays the same, so lastcost
+usually wins. Lastcost could be lower priority, but a better design would be
+nice.
+
+=item *
+
+Guarantee 1 states that price sources will never change prices on their own.
+Further testing in the wild has shown that this is desirable within a record,
+but not when copying items from one record to another within a workflow.
+
+Specifically when changing from sales to purchase records prices don't make
+sense anymore. The guarantees should be updated to reflect this and
+transposition guidelines should be documented.
+
+The previously mentioned linked prices can emulated by allowing price sources
+to set a new price when changing to a new record in the workflow. The decision
+about whether a price is eligable to be set can be suggested by the price
+source implementation but is ultimately up to the surrounding framework, which
+can make this configurable.
+
+=item *
+
+Prices were originally planned as a context element rather than a modal popup.
+It would be great to have this now with better framework.
+
+=item *
+
+Large records (30 positions or more) in combination with complicated price
+sources run into n+1 problems. There should be an extra hook that allows price
+source implementations to make bulk calculations before the actual position loop.
+
+=item *
+
+Prices have defined information channels for missing and invalid, but it would
+be deriable to have more information flow. For example a limited offer might
+expire in three days while the record is valid for 20 days. THis mismatch is
+impossible to resolve automatically, but informing the user about it would be a
+nice thing.
+
+This can also extend to diagnostics on class level, where implementations can
+call attention to likely misconfigurations.
=back