X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FPriceSource.pm;h=05168ce679f1d7ee3e5caaf4dd4cf5d07b934fac;hb=f05bd96b031d7d4ffaf4804704684ae929a0890a;hp=9a3a978c5ce1debd1292318bdf71feefe9d751e8;hpb=eebe8e90991eacadb6fbd20a648c152017a620c7;p=kivitendo-erp.git diff --git a/SL/PriceSource.pm b/SL/PriceSource.pm index 9a3a978c5..05168ce67 100644 --- a/SL/PriceSource.pm +++ b/SL/PriceSource.pm @@ -3,48 +3,96 @@ package SL::PriceSource; use strict; use parent 'SL::DB::Object'; use Rose::Object::MakeMethods::Generic ( - scalar => [ qw(record_item) ], + 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); +use List::UtilsBy qw(min_by max_by); use SL::PriceSource::ALL; use SL::PriceSource::Price; use SL::Locale::String; -sub all_price_sources { +sub init_all_price_sources { my ($self) = @_; - return map { - $_->new(record_item => $self->record_item) - } SL::PriceSource::ALL->all_price_sources + [ map { + $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; + + ${ $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); + + $source_object + ? $source_object->price_from_source($source, $spec) + : empty_price(); + } +} + +sub discount_from_source { + my ($self, $source) = @_; + return empty_discount() if !$source; + + ${ $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); - my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name); + $source_object + ? $source_object->discount_from_source($source, $spec) + : empty_discount(); + } +} + +sub init_available_prices { + [ map { $_->available_prices } $_[0]->all_price_sources ]; +} - return $class - ? $class->new(record_item => $self->record_item)->price_from_source($source, $spec) - : empty_price(); +sub init_available_discounts { + return [] if $_[0]->record_item->part->not_discountable; + [ map { $_->available_discounts } $_[0]->all_price_sources ]; } -sub available_prices { - map { $_->available_prices } $_[0]->all_price_sources; +sub init_best_price { + min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources; } -sub best_price { - min_by { $_->price } map { $_->best_price } $_[0]->all_price_sources; +sub init_best_discount { + max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources; } sub empty_price { SL::PriceSource::Price->new( - source => '', description => t8('None (PriceSource)'), ); } +sub empty_discount { + SL::PriceSource::Discount->new( + description => t8('None (PriceSource Discount)'), + ); +} + 1; __END__ @@ -55,44 +103,272 @@ __END__ SL::PriceSource - mixin for price_sources in record items -=head1 SYNOPSIS +=head1 DESCRIPTION - # in record item class +PriceSource is an interface that allows generic algorithms to be plugged +together to calculate available prices for a position in a record. - use SL::PriceSource; +Each algorithm can access details of the record to realize dependencies on +part, customer, vendor, date, quantity etc, which was previously not possible. - # later on: +=head1 BACKGROUND AND PHILOSOPHY - $record_item->all_price_sources - $record_item->price_source # get - $record_item->price_source($c) # set +sql ledger and subsequently Lx-Office had three prices per part: sellprice, +listprice and lastcost. When adding an item to a record, the applicable price +was copied and after that it was free to be changed. - $record_item->update_price_source # set price to calculated +Later on additional things were added. Various types of discount, vendor pricelists +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 +available price from the database, and when to leave the user entered price. -=head1 DESCRIPTION +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. + +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 +position in a quotation has, and wanted information available when prices +changed to make better informed choices about sales later in the workflow. + +Price sources now extend the previous pricing by attaching a source to every +price in records. The information it provides are: + +=over 4 + +=item 1. + +Where did this price originate? + +=item 2. + +If this price would be calculated today, is it still the same as it was when +this record was created? + +=item 3. + +If I want to price an item in this record now, which prices are available? + +=item 4. + +Which one is the "best"? + +=back + +=head1 GUARANTEES + +To ensure price source prices are comprehensible and reproducible, some +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. + +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. + +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. + +=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 +information about rising or falling prices. + +=head1 STRUCTURE + +Price sources are managed by this package (L), and all +external access should be by using its interface. + +Each source is an instance of L and the available +implementations are recorded in L. Prices and discounts +returned by interface methods are instances of L and +L. + +Returned prices and discounts should be checked for entries in C and +C, see documentation in their classes. + +=head1 INTERFACE METHODS + +=over 4 + +=item C + +C must contain both C and C. C does +not have to be registered in C. + +=item C + +Attempts to retrieve a formerly calculated price with the same conditions + +=item C + +Attempts to retrieve a formerly calculated discount with the same conditions + +=item C + +Returns all available prices. + +=item C + +Returns all available discounts. + +=item C + +Attempts to get the best available price. returns L if no price is +found. + +=item C + +Attempts to get the best available discount. returns L if no +discount is found. + +=item C + +A special empty price that does not change the previously entered price and +opens the price field to manual changes. + +=item C + +A special empty discount that does not change the previously entered discount +and opens the discount field to manual changes. + +=item C + +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, +L, +L, +L + +=head1 BUGS AND CAVEATS + +=over 4 + +=item * + +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). + +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 +properties. + +=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 single +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. + +=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. + +=item * + +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. -This mixin provides a way to use price_source objects from within a record item. -Record items in this contest mean OrderItems, InvoiceItems and -DeliveryOrderItems. +=item * -=head1 FUNCTIONS +Prices were originally planned as a context element rather than a modal popup. +It would be great to have this now with better framework. -price_sources +=item * -returns a list of price_source objects which are created with the current record -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. -active_price_source +=item * -returns the object representing the currently chosen price_source method or -undef if custom price is chosen. Note that this must not necessarily be the -active price, if something affecting the price_source has changed, the price -calculated can differ from the price in the record. It is the responsibility of -the implementing code to decide what to do in this case. +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. -=head1 BUGS +This can also extend to diagnostics on class level, where implementations can +call attention to likely misconfigurations. -None yet. :) +=back =head1 AUTHOR