1 package SL::PriceSource;
 
   4 use parent 'SL::DB::Object';
 
   5 use Rose::Object::MakeMethods::Generic (
 
   6   scalar => [ qw(record_item record) ],
 
   7   'array --get_set_init' => [ qw(all_price_sources) ],
 
  10 use List::UtilsBy qw(min_by max_by);
 
  11 use SL::PriceSource::ALL;
 
  12 use SL::PriceSource::Price;
 
  13 use SL::Locale::String;
 
  15 sub init_all_price_sources {
 
  19     $_->new(record_item => $self->record_item, record => $self->record)
 
  20   } SL::PriceSource::ALL->all_enabled_price_sources ]
 
  23 sub price_from_source {
 
  24   my ($self, $source) = @_;
 
  25   my ($source_name, $spec) = split m{/}, $source, 2;
 
  27   my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
 
  30     ? $class->new(record_item => $self->record_item, record => $self->record)->price_from_source($source, $spec)
 
  34 sub discount_from_source {
 
  35   my ($self, $source) = @_;
 
  36   my ($source_name, $spec) = split m{/}, $source, 2;
 
  38   my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
 
  41     ? $class->new(record_item => $self->record_item, record => $self->record)->discount_from_source($source, $spec)
 
  45 sub available_prices {
 
  46   map { $_->available_prices } $_[0]->all_price_sources;
 
  49 sub available_discounts {
 
  50   return if $_[0]->record_item->part->not_discountable;
 
  51   map { $_->available_discounts } $_[0]->all_price_sources;
 
  55   min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
 
  59   max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
 
  63   SL::PriceSource::Price->new(
 
  64     description => t8('None (PriceSource)'),
 
  69   SL::PriceSource::Discount->new(
 
  70     description => t8('None (PriceSource Discount)'),
 
  82 SL::PriceSource - mixin for price_sources in record items
 
  86 PriceSource is an interface that allows generic algorithms to be plugged
 
  87 together to calculate available prices for a position in a record.
 
  89 Each algorithm can access details of the record to realize dependencies on
 
  90 part, customer, vendor, date, quantity etc, which was previously not possible.
 
  92 =head1 BACKGROUND AND PHILOSOPHY
 
  94 sql ledger and subsequently Lx-Office had three prices per part: sellprice,
 
  95 listprice and lastcost. When adding an item to a record, the applicable price
 
  96 was copied and after that it was free to be changed.
 
  98 Later on additional things were added. Various types of discount, vendor pricelists
 
  99 and the infamous price groups. The problem was not that those didn't work, the
 
 100 problem was they had to guess too much when to change a price with the
 
 101 available price from the database, and when to leave the user entered price.
 
 103 The result was that the price of an item in a record seemed to change on a
 
 104 whim, and the origin of the price itself being opaque.
 
 106 Unrelated to that, users asked for more ways to store special prices, based on
 
 107 qty (block pricing, bulk discount), based on date (special offers), based on
 
 108 customers (special terms), up to full blown calculation modules.
 
 110 On a third front sales personnel asked for ways to see what price options a
 
 111 position in a quotation has, and wanted information available when prices
 
 112 changed to make better informed choices about sales later in the workflow.
 
 114 Price sources now extend the previous pricing by attaching a source to every
 
 115 price in records. The information it provides are:
 
 121 Where did this price originate?
 
 125 If this price would be calculated today, is it still the same as it was when
 
 126 this record was created?
 
 130 If I want to price an item in this record now, which prices are available?
 
 134 Which one is the "best"?
 
 140 To ensure price source prices are comprehensible and reproducible, some
 
 141 invariants are guaranteed:
 
 147 Price sources will never on their own change a price. They will offer options,
 
 148 and it is up to the user to change a price.
 
 152 If a price is set from a source, it is read only. A price edited manually is by
 
 153 definition not a sourced price.
 
 157 A price should be able to repeat the calculations done to arrive at the price
 
 158 when it was first used. If these calculations are no longer applicable (special
 
 159 offer expired) this should be signalled. If the calculations result in a
 
 160 different price, this should be signalled. If the calculations fail (needed
 
 161 information is no longer present) this must be signalled.
 
 165 The first point creates user security by never changing a price for them
 
 166 without their explicit consent, eliminating all problems originating from
 
 167 trying to be smart. The second and third one ensure that later on the
 
 168 calculation can be repeated so that invalid prices can be caught (because for
 
 169 example the special offer is no longer valid), and so that sales personnel have
 
 170 information about rising or falling prices.
 
 174 Price sources are managed by this package (L<SL::PriceSource>), and all
 
 175 external access should be by using it's interface.
 
 177 Each source is an instance of L<SL::PriceSource::Base> and the available
 
 178 implementations are recorded in L<SL::PriceSource::ALL>. Prices and discounts
 
 179 returned by interface methods are instances of L<SL::PriceSource::Price> and
 
 180 L<SL::PriceSource::Discout>.
 
 182 Returned prices and discounts should be checked for entries in C<invalid> and
 
 183 C<missing>, see documentation in their classes.
 
 185 =head1 INTERFACE METHODS
 
 191 C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
 
 192 not have to be registered in C<record>.
 
 194 =item C<price_from_source>
 
 196 Attempts to retrieve a formerly calculated price with the same conditions
 
 198 =item C<discount_from_source>
 
 200 Attempts to retrieve a formerly calculated discount with the same conditions
 
 202 =item C<available_prices>
 
 204 Returns all available prices.
 
 206 =item C<available_discounts>
 
 208 Returns all available discounts.
 
 212 Attempts to get the best available price. returns L<empty_price> if no price is found.
 
 214 =item C<best_discount>
 
 216 Attempts to get the best available discount. returns L<empty_discount> if no discount is found.
 
 220 A special empty price, that does not change the previously entered price, and
 
 221 opens the price field to manual changes.
 
 223 =item C<empty_discount>
 
 225 A special empty discount, that does not change the previously entered discount, and
 
 226 opens the discount field to manual changes.
 
 232 L<SL::PriceSource::Base>,
 
 233 L<SL::PriceSource::Price>,
 
 234 L<SL::PriceSource::Discount>,
 
 235 L<SL::PriceSource::ALL>
 
 237 =head1 BUGS AND CAVEATS
 
 243 The current model of price sources requires a record and a record_item for
 
 244 every price calculation. This means that price structures can never be used
 
 245 when no record is available, such as calculation the worth of assembly rows.
 
 247 A possible solution is to either split price sources into simple and complex
 
 248 ones (where the former do not require records).
 
 250 Another would be to have default values for the input normally taken from
 
 251 records (like qty defaulting to 1).
 
 253 A last one would be to provide an alternative input channel for needed
 
 258 Discount sources were implemented as a copy of the prices with slightly
 
 259 different semantics. Need to do a real design. A requirement is, that a single
 
 260 source can provide both prices and discounts (needed for price_rules).
 
 264 Priorities are implemented ad hoc. The semantics which are chosen by the "best"
 
 265 accessors are unintuitive because they do not guarantee anything. Better
 
 266 terminology might help.
 
 270 It is currently not possible to link a price to the price of the generating
 
 271 record_item (i.e. the price of a delivery order item to the order item it was
 
 272 generated from). This is crucial to enterprises that calculate all their prices
 
 273 in orders, and update those after they made delivery orders.
 
 277 Currently it is only possible to provide additional prices, but not to restrict
 
 278 prices. Potential scenarios include credit limit customers which do not receive
 
 279 benefits from sales, or general ALLOW, DENY order calculation.
 
 283 Composing price sources is disallowed for clarity, but all price sources need
 
 284 to be aware of units and price_factors. This is madness.
 
 288 A common complaint is that prices from certain vendors are always negotiated
 
 289 and should use a default value but must be editable (like free prices) by
 
 290 default. This should be orthogonal for all prices.
 
 294 The current implementation of lastcost is useless. Since it's one of the
 
 295 master_data prices it will always compete with listprice. But in real scenarios
 
 296 the listprice tends to go up, while lastcost stays the same, so lastcost
 
 297 usually wins. Lastcost could be lower priority, but a better design would be
 
 304 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>