PriceSource: Dokumentation
authorSven Schöling <s.schoeling@linet-services.de>
Mon, 28 Jul 2014 15:56:17 +0000 (17:56 +0200)
committerSven Schöling <s.schoeling@linet-services.de>
Thu, 18 Dec 2014 15:18:21 +0000 (16:18 +0100)
SL/PriceSource.pm
SL/PriceSource/Base.pm
SL/PriceSource/Price.pm

index 7a631ba..470182c 100644 (file)
@@ -40,7 +40,6 @@ sub best_price {
 
 sub empty_price {
   SL::PriceSource::Price->new(
-    source      => '',
     description => t8('None (PriceSource)'),
   );
 }
@@ -55,40 +54,98 @@ __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 dependancies on
+part, customer, vendor, date, quantity etc, which was previously not possible.
 
-  # later on:
+=head1 BACKGROUND AND PHILOSOPY
 
-  $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. At the moment a part is loaded into a record, the
+applicable price is copied and after that free to be changed.
 
-  $record_item->update_price_source # set price to calculated
+Later on additional things joined. Various types of discount, vendor pricelists
+and the infamous price groups. The problem is not that those didn't work, the
+problem is, that they had to guess to much when to change a price with the
+available price from database, and when to leave the user entered price.
 
-=head1 DESCRIPTION
+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 a price
+offer changed.
+
+Price sources put that together by making some compromises:
+
+=over 4
+
+=item 1.
+
+Only change the price on creation of a position or when asked to.
+
+=item 2.
+
+Either set the price from a price source and let it be read only, or use a free
+price.
+
+=item 3.
+
+Save the origin of each price with the record so that the calculation can be
+reproduced.
+
+=item 4.
+
+Make price calculation flexible and pluggable.
+
+=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. The fourth point ensures that
+insular calculation processes can be developed independant of the core code.
+
+=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
+
+=item C<available_prices>
+
+Returns all available prices.
+
+=item C<best_price>
 
-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.
+Attempts to get the best available price. returns L<empty_price> if no price is found.
 
-=head1 FUNCTIONS
+=item C<empty_price>
 
-price_sources
+A special empty price, that does not change the previously entered price, and
+opens the price field to manual changes.
 
-returns a list of price_source objects which are created with the current record
-item.
+=back
 
-active_price_source
+=head1 SEE ALSO
 
-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.
+L<SL::PriceSource::Base>,
+L<SL::PriceSource::Price>,
+L<SL::PriceSource::ALL>
 
 =head1 BUGS
 
index 618512d..b095684 100644 (file)
@@ -29,40 +29,76 @@ __END__
 
 =head1 NAME
 
-SL::PriceSource::Base - <oneliner description>
+SL::PriceSource::Base - this is the base class for price source adapters
 
 =head1 SYNOPSIS
 
-  # in consuming module
-# TODO: thats bullshit, theres no need to have this pollute the namespace
-# make a manager that handles this
+  # working example adapter:
+  package SL::PriceSource::FiveOnEverything;
 
-  my @list_of_price_sources = $record_item->price_sources;
-  for (@list_of_price_sources) {
-    my $internal_name   = $_->name;
-    my $translated_name = $_->description;
-    my $price           = $_->price;
+  use parent qw(SL::PriceSource::Base);
+
+  # used as internal identifier
+  sub name { 'simple' }
+
+  # used in frontend to signal where this comes from
+  sub description { t8('Simple') }
+
+  my $price = SL::PriceSource::Price->new(
+    price        => 5,
+    description  => t8('Only today 5$ on everything!'),
+    price_source => $self,
+  );
+
+  # give list of prices that this
+  sub available_prices {
+    return ($price);
   }
 
-  $record_item->set_active_price_source($price_source)  # equivalent to:
-  $record_item->active_price_source($price_source->name);
-  $record_item->sellprice($price_source->price);
+  sub best_price {
+    return $price;
+  }
 
-  # for finer control
-  $price_source->needed_params
-  $price_source->supported_params
+  sub price_from_source {
+    return $price;
+  }
 
 =head1 DESCRIPTION
 
-PriceSource is an interface that allows generic algorithms to be used, to
-calculate a price for a position in a record.
+See L<SL::PriceSource> for information about the mechanism.
+
+This is the base class for a price source algorithm. To play well, you'll have
+to implement a number of interface methods and be aware of a number of corner
+conditions.
+
+=head1 AVAILABLE METHODS
+
+=over 4
+
+=item C<record_item>
+
+=item C<record>
+
+C<record> can be any one of L<SL::DB::Order>, L<SL::DB::DeliveryOrder>,
+L<SL::DB::Invoice>, L<SL::DB::PurchaseInvoice>. C<record_item> is of the
+corresponding position type.
+
+You can assume that both are filled with all information available at the time.
+C<part> and C<customer>/C<vendor> as well as C<is_sales> can be relied upon. You must NOT
+rely on both being linked together, in particular
+
+  $self->record_item->record   # don't do that
 
-If any such price_source algorithm is known to the system, a user can chose
-which of them should be used to claculate the price displayed in the record.
+is not guaranteed to work.
 
-The algorithm is saved togetherwith the target price, so that changes in the
-record can recalculate the price accordingly, and otherwise manual changes to
-the price can reset the price_source used to custom (aka no price_source).
+Also these are copies and not the original documents. Do not try to change
+anything and do not save those.
+
+=item C<part>
+
+Shortcut to C<< record_item->part >>
+
+=back
 
 =head1 INTERFACE METHODS
 
@@ -70,32 +106,89 @@ the price can reset the price_source used to custom (aka no price_source).
 
 =item C<name>
 
-Should return a unique internal name. Should be entered in
-L<SL::PriceSource::ALL> so that a name_to_class lookup works.
+Must return a unique internal name. Must be entered in
+L<SL::PriceSource::ALL>.
 
 =item C<description>
 
-Should return a translated name.
+Must return a translated name to be used in frontend. Will be used, to
+distinguish the origin of different prices.
+
+=item C<available_prices>
 
-=item C<needed_params>
+Must return a list of all prices that you algorithm can recommend the user
+for the current situation. Each price must have a unique spec that can be used
+to recreate it later. Try to be brief, no one needs 20 different price
+suggestions.
 
-Should return a list of elements that a record_item NEEDS to be used with this calulation.
+=item C<best_price>
 
-Both C<needed_params> nad C<supported_params> are purely informational at this point.
+Must return what you think of as the best matching price in your
+C<available_prices>. This does not have to be the lowest price, but it will be
+compared later to other price sources, and the lowest will be set.
 
-=item C<supported_params>
+=item C<price_from_source SOURCE, SPEC>
 
-Should return a list of elements that a record_item MAY HAVE to be used with this calulation.
+Must recreate the price from C<SPEC> and return. For reference, the complete
+C<SOURCE> entry from C<record_item.active_price_source> is included.
 
-Both C<needed_params> nad C<supported_params> are purely informational at this point.
+Note that constraints from the rest of the C<record> do not apply anymore. If
+information needed for the retrieval can be deleted elsewhere, then you must
+guard against that.
 
-=item C<price>
+If the price for the same coditions changed, return the new price. It will be
+presented as an option to the user if the record is still editable.
 
-Calculate a price and return. Do not mutate the record_item. Should will return
-undef if price is not applicable to the current record_item.
+If the price is not valid anymore or not reconstructable, return a price with
+C<price_source> and C<spec> set to the same values as before but with
+C<invalid> or C<missing> set.
 
 =back
 
+=head1 TRAPS AND CORNER CASES
+
+=over 4
+
+=item *
+
+Be aware that all 8 types of record will be passed to your algorithm. If you
+don't serve some of them, just return emptry lists on C<available_prices> and
+C<best_price>
+
+=item *
+
+Information in C<record> might be missing. Especially on newly or automatically
+created records there might be fields not set at all.
+
+=item *
+
+Records will not be calculated. If you need tax data or position totals, you
+need to invoke that for yourself.
+
+=item *
+
+Accessor methods might not be present in some of the record types.
+
+=item *
+
+You do not need to do price factor and row discount calculation. These will be
+done automatically afterwards. You do have to include customer/vendor discount
+if your price interacts with those.
+
+=item *
+
+The price field in purchase records is still C<sellprice>.
+
+=item *
+
+C<source> and C<spec> are tainted. If you store data directly in C<spec>, sanitize.
+
+=head1 SEE ALSO
+
+L<SL::PriceSource>,
+L<SL::PriceSource::Price>,
+L<SL::PriceSource::ALL>
+
 =head1 BUGS
 
 None yet. :)
index 593c5ff..23d3a9e 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use parent 'SL::DB::Object';
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(price description spec price_source) ],
+  scalar => [ qw(price description spec price_source invalid missing) ],
   array => [ qw(depends_on) ]
 );
 
@@ -32,3 +32,104 @@ sub to_str {
 }
 
 1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+SL::PriceSource::Price - contrainer to pass calculated prices around
+
+=head1 SYNOPSIS
+
+  # in PriceSource::Base implementation
+  $price = SL::PriceSource::Price->new(
+    price        => 10.3,
+    spec         => '10.3', # something you can easily parse later
+    description  => t8('Fix price 10.3'),
+    price_source => $self,
+  )
+
+  # special empty price in SL::PriceSource
+  SL::PriceSource::Price->new(
+    description => t8('None (PriceSource)'),
+  );
+
+  # invalid price
+  SL::PriceSource::Price->new(
+    price        => $original_price,
+    spec         => $original_spec,
+    description  => $original_description,
+    invalid      => t8('Offer expired #1 weeks ago', $dt->delta_weeks),
+    price_source => $self,
+  );
+
+  # missing price
+  SL::PriceSource::Price->new(
+    price        => $original_price,              # will keep last entered price
+    spec         => $original_spec,
+    description  => '',
+    missing      => t8('Um, sorry, cannot find that one'),
+    price_source => $self,
+  );
+
+
+=head1 DESCRIPTION
+
+See L<SL::PriceSource> for information about the mechanism.
+
+This is a container for prices that are generated by L<SL::PriceSource::Base>
+implementations.
+
+=head1 CONSTRUCTOR FIELDS
+
+=over 4
+
+=item C<price>
+
+The price. A price of 0 is special and is considered undesirable. If passed as
+part of C<available_prices> it will be filtered out. If returned as
+C<best_price> or C<price_from_source> it will be warned about.
+
+=item C<spec>
+
+A unique string that can later be understood by the creating implementation.
+Can be empty if the implementation only supports one price for a given
+record_item.
+
+=item C<description>
+
+A localized short description of the origins of this price.
+
+=item C<price_source>
+
+A ref to the creating algorithm.
+
+=item C<missing>
+
+OPTIONAL. Both indicator and localized message that the price with this spec
+could not be reproduced and should be changed.
+
+=item C<invalid>
+
+OPTIONAL. Both indicator and localized message that the conditions for this
+price are no longer valid, and that the price should be changed.
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::PriceSource>,
+L<SL::PriceSource::Base>,
+L<SL::PriceSource::ALL>
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 AUTHOR
+
+Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
+
+=cut