Projekt

Allgemein

Profil

Herunterladen (11,5 KB) Statistiken
| Zweig: | Markierung: | Revision:
eebe8e90 Sven Schöling
package SL::PriceSource;

use strict;
use parent 'SL::DB::Object';
use Rose::Object::MakeMethods::Generic (
ab719aed Sven Schöling
scalar => [ qw(record_item record fast) ],
259a5727 Sven Schöling
'scalar --get_set_init' => [ qw(
best_price best_discount
) ],
'array --get_set_init' => [ qw(
all_price_sources
available_prices available_discounts
) ],
eebe8e90 Sven Schöling
);

89b26688 Sven Schöling
use List::UtilsBy qw(min_by max_by);
eebe8e90 Sven Schöling
use SL::PriceSource::ALL;
use SL::PriceSource::Price;
use SL::Locale::String;

1c311d64 Sven Schöling
sub init_all_price_sources {
eebe8e90 Sven Schöling
my ($self) = @_;

1c311d64 Sven Schöling
[ map {
259a5727 Sven Schöling
$self->price_source_by_class($_);
1c311d64 Sven Schöling
} SL::PriceSource::ALL->all_enabled_price_sources ]
eebe8e90 Sven Schöling
}

259a5727 Sven Schöling
sub price_source_by_class {
my ($self, $class) = @_;
return unless $class;

$self->{price_source_by_name}{$class} //=
ab719aed Sven Schöling
$class->new(record_item => $self->record_item, record => $self->record, fast => $self->fast);
259a5727 Sven Schöling
}

eebe8e90 Sven Schöling
sub price_from_source {
my ($self, $source) = @_;
259a5727 Sven Schöling
return empty_price() if !$source;
eebe8e90 Sven Schöling
259a5727 Sven Schöling
${ $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);
eebe8e90 Sven Schöling
259a5727 Sven Schöling
$source_object
? $source_object->price_from_source($source, $spec)
: empty_price();
}
eebe8e90 Sven Schöling
}

e1c3b6b7 Bernd Bleßmann
sub discount_from_source {
my ($self, $source) = @_;
259a5727 Sven Schöling
return empty_discount() if !$source;
e1c3b6b7 Bernd Bleßmann
259a5727 Sven Schöling
${ $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);
e1c3b6b7 Bernd Bleßmann
259a5727 Sven Schöling
$source_object
? $source_object->discount_from_source($source, $spec)
: empty_discount();
}
e1c3b6b7 Bernd Bleßmann
}

259a5727 Sven Schöling
sub init_available_prices {
[ map { $_->available_prices } $_[0]->all_price_sources ];
eebe8e90 Sven Schöling
}

259a5727 Sven Schöling
sub init_available_discounts {
return [] if $_[0]->record_item->part->not_discountable;
[ map { $_->available_discounts } $_[0]->all_price_sources ];
89b26688 Sven Schöling
}

259a5727 Sven Schöling
sub init_best_price {
9f666261 Sven Schöling
min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
89b26688 Sven Schöling
}

259a5727 Sven Schöling
sub init_best_discount {
9f666261 Sven Schöling
max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
eebe8e90 Sven Schöling
}

sub empty_price {
SL::PriceSource::Price->new(
description => t8('None (PriceSource)'),
);
}

e1c3b6b7 Bernd Bleßmann
sub empty_discount {
SL::PriceSource::Discount->new(
description => t8('None (PriceSource Discount)'),
);
}

eebe8e90 Sven Schöling
1;

__END__

=encoding utf-8

=head1 NAME

SL::PriceSource - mixin for price_sources in record items

418f0e70 Sven Schöling
=head1 DESCRIPTION
eebe8e90 Sven Schöling
418f0e70 Sven Schöling
PriceSource is an interface that allows generic algorithms to be plugged
together to calculate available prices for a position in a record.
eebe8e90 Sven Schöling
049e49fe Sven Schöling
Each algorithm can access details of the record to realize dependencies on
418f0e70 Sven Schöling
part, customer, vendor, date, quantity etc, which was previously not possible.
eebe8e90 Sven Schöling
fd6f0f82 Geoffrey Richardson
=head1 BACKGROUND AND PHILOSOPHY
eebe8e90 Sven Schöling
418f0e70 Sven Schöling
sql ledger and subsequently Lx-Office had three prices per part: sellprice,
7801c6c2 Sven Schöling
listprice and lastcost. When adding an item to a record, the applicable price
was copied and after that it was free to be changed.
eebe8e90 Sven Schöling
fd6f0f82 Geoffrey Richardson
Later on additional things were added. Various types of discount, vendor pricelists
7801c6c2 Sven Schöling
and the infamous price groups. The problem was not that those didn't work, the
problem was they had to guess too much when to change a price with the
fd6f0f82 Geoffrey Richardson
available price from the database, and when to leave the user entered price.
eebe8e90 Sven Schöling
7801c6c2 Sven Schöling
The result was that the price of an item in a record seemed to change on a
whim, and the origin of the price itself being opaque.

418f0e70 Sven Schöling
Unrelated to that, users asked for more ways to store special prices, based on
qty (block pricing, bulk discount), based on date (special offers), based on
customers (special terms), up to full blown calculation modules.

On a third front sales personnel asked for ways to see what price options a
7801c6c2 Sven Schöling
position in a quotation has, and wanted information available when prices
changed to make better informed choices about sales later in the workflow.
418f0e70 Sven Schöling
7801c6c2 Sven Schöling
Price sources now extend the previous pricing by attaching a source to every
price in records. The information it provides are:
418f0e70 Sven Schöling
=over 4

=item 1.

7801c6c2 Sven Schöling
Where did this price originate?
418f0e70 Sven Schöling
=item 2.

7801c6c2 Sven Schöling
If this price would be calculated today, is it still the same as it was when
this record was created?
418f0e70 Sven Schöling
=item 3.

7801c6c2 Sven Schöling
If I want to price an item in this record now, which prices are available?
418f0e70 Sven Schöling
=item 4.

7801c6c2 Sven Schöling
Which one is the "best"?

=back

=head1 GUARANTEES

9a1cccab Geoffrey Richardson
To ensure price source prices are comprehensible and reproducible, some
7801c6c2 Sven Schöling
invariants are guaranteed:

=over 4

=item 1.

Price sources will never on their own change a price. They will offer options,
and it is up to the user to change a price.

=item 2.

49f71dba Sven Schöling
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.

34cc5469 Sven Schöling
A price that is not set from a source will not have any of this.
7801c6c2 Sven Schöling
=item 3.

A price should be able to repeat the calculations done to arrive at the price
when it was first used. If these calculations are no longer applicable (special
offer expired) this should be signalled. If the calculations result in a
different price, this should be signalled. If the calculations fail (needed
information is no longer present) this must be signalled.
418f0e70 Sven Schöling
=back

The first point creates user security by never changing a price for them
without their explicit consent, eliminating all problems originating from
trying to be smart. The second and third one ensure that later on the
calculation can be repeated so that invalid prices can be caught (because for
example the special offer is no longer valid), and so that sales personnel have
7801c6c2 Sven Schöling
information about rising or falling prices.

=head1 STRUCTURE

Price sources are managed by this package (L<SL::PriceSource>), and all
34cc5469 Sven Schöling
external access should be by using its interface.
7801c6c2 Sven Schöling
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
34cc5469 Sven Schöling
L<SL::PriceSource::Discount>.
7801c6c2 Sven Schöling
Returned prices and discounts should be checked for entries in C<invalid> and
C<missing>, see documentation in their classes.
418f0e70 Sven Schöling
=head1 INTERFACE METHODS

=over 4

=item C<new PARAMS>

C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
not have to be registered in C<record>.

=item C<price_from_source>

Attempts to retrieve a formerly calculated price with the same conditions

e1c3b6b7 Bernd Bleßmann
=item C<discount_from_source>

Attempts to retrieve a formerly calculated discount with the same conditions

418f0e70 Sven Schöling
=item C<available_prices>

Returns all available prices.

e1c3b6b7 Bernd Bleßmann
=item C<available_discounts>

Returns all available discounts.

418f0e70 Sven Schöling
=item C<best_price>
eebe8e90 Sven Schöling
34cc5469 Sven Schöling
Attempts to get the best available price. returns L<empty_price> if no price is
found.
eebe8e90 Sven Schöling
e1c3b6b7 Bernd Bleßmann
=item C<best_discount>

34cc5469 Sven Schöling
Attempts to get the best available discount. returns L<empty_discount> if no
discount is found.
e1c3b6b7 Bernd Bleßmann
418f0e70 Sven Schöling
=item C<empty_price>
eebe8e90 Sven Schöling
34cc5469 Sven Schöling
A special empty price that does not change the previously entered price and
418f0e70 Sven Schöling
opens the price field to manual changes.
eebe8e90 Sven Schöling
e1c3b6b7 Bernd Bleßmann
=item C<empty_discount>

34cc5469 Sven Schöling
A special empty discount that does not change the previously entered discount
and opens the discount field to manual changes.
e1c3b6b7 Bernd Bleßmann
ab719aed Sven Schöling
=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.

418f0e70 Sven Schöling
=back
eebe8e90 Sven Schöling
ab719aed Sven Schöling
418f0e70 Sven Schöling
=head1 SEE ALSO
eebe8e90 Sven Schöling
418f0e70 Sven Schöling
L<SL::PriceSource::Base>,
L<SL::PriceSource::Price>,
e1c3b6b7 Bernd Bleßmann
L<SL::PriceSource::Discount>,
418f0e70 Sven Schöling
L<SL::PriceSource::ALL>
eebe8e90 Sven Schöling
d770b5b3 Sven Schöling
=head1 BUGS AND CAVEATS
eebe8e90 Sven Schöling
d770b5b3 Sven Schöling
=over 4

=item *

7801c6c2 Sven Schöling
The current model of price sources requires a record and a record_item for
every price calculation. This means that price structures can never be used
when no record is available, such as calculation the worth of assembly rows.

A possible solution is to either split price sources into simple and complex
ones (where the former do not require records).

30a5c527 Sven Schöling
Another would be to have default values for the input normally taken from
7801c6c2 Sven Schöling
records (like qty defaulting to 1).
d770b5b3 Sven Schöling
7801c6c2 Sven Schöling
A last one would be to provide an alternative input channel for needed
properties.

=item *

Discount sources were implemented as a copy of the prices with slightly
30a5c527 Sven Schöling
different semantics. Need to do a real design. A requirement is, that a single
7801c6c2 Sven Schöling
source can provide both prices and discounts (needed for price_rules).

=item *

Priorities are implemented ad hoc. The semantics which are chosen by the "best"
accessors are unintuitive because they do not guarantee anything. Better
terminology might help.

=item *

It is currently not possible to link a price to the price of the generating
record_item (i.e. the price of a delivery order item to the order item it was
generated from). This is crucial to enterprises that calculate all their prices
in orders, and update those after they made delivery orders.
d770b5b3 Sven Schöling
=item *

Currently it is only possible to provide additional prices, but not to restrict
prices. Potential scenarios include credit limit customers which do not receive
benefits from sales, or general ALLOW, DENY order calculation.

7801c6c2 Sven Schöling
=item *

9a1cccab Geoffrey Richardson
Composing price sources is disallowed for clarity, but all price sources need
30a5c527 Sven Schöling
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.
7801c6c2 Sven Schöling
8cf8798d Sven Schöling
=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.

34cc5469 Sven Schöling
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.

fed564aa Sven Schöling
=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.

34cc5469 Sven Schöling
=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.

29dad41c Sven Schöling
=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.

d770b5b3 Sven Schöling
=back
eebe8e90 Sven Schöling
=head1 AUTHOR

Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>

=cut