Consolidation and extended test runs
[kivitendo-erp.git] / SL / PriceSource.pm
1 package SL::PriceSource;
2
3 use strict;
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) ],
8 );
9
10 use List::UtilsBy qw(min_by max_by);
11 use SL::PriceSource::ALL;
12 use SL::PriceSource::Price;
13 use SL::Locale::String;
14
15 sub init_all_price_sources {
16   my ($self) = @_;
17
18   [ map {
19     $_->new(record_item => $self->record_item, record => $self->record)
20   } SL::PriceSource::ALL->all_enabled_price_sources ]
21 }
22
23 sub price_from_source {
24   my ($self, $source) = @_;
25   my ($source_name, $spec) = split m{/}, $source, 2;
26
27   my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
28
29   return $class
30     ? $class->new(record_item => $self->record_item, record => $self->record)->price_from_source($source, $spec)
31     : empty_price();
32 }
33
34 sub available_prices {
35   map { $_->available_prices } $_[0]->all_price_sources;
36 }
37
38 sub available_discounts {
39   return if $_[0]->record_item->part->not_discountable;
40   map { $_->available_discounts } $_[0]->all_price_sources;
41 }
42
43 sub best_price {
44   min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
45 }
46
47 sub best_discount {
48   max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
49 }
50
51 sub empty_price {
52   SL::PriceSource::Price->new(
53     description => t8('None (PriceSource)'),
54   );
55 }
56
57 1;
58
59 __END__
60
61 =encoding utf-8
62
63 =head1 NAME
64
65 SL::PriceSource - mixin for price_sources in record items
66
67 =head1 DESCRIPTION
68
69 PriceSource is an interface that allows generic algorithms to be plugged
70 together to calculate available prices for a position in a record.
71
72 Each algorithm can access details of the record to realize dependencies on
73 part, customer, vendor, date, quantity etc, which was previously not possible.
74
75 =head1 BACKGROUND AND PHILOSOPHY
76
77 sql ledger and subsequently Lx-Office had three prices per part: sellprice,
78 listprice and lastcost. At the moment a part is loaded into a record, the
79 applicable price is copied and after that it is free to be changed.
80
81 Later on additional things were added. Various types of discount, vendor pricelists
82 and the infamous price groups. The problem is not that those didn't work, the
83 problem is, that they had to guess too much when to change a price with the
84 available price from the database, and when to leave the user entered price.
85
86 Unrelated to that, users asked for more ways to store special prices, based on
87 qty (block pricing, bulk discount), based on date (special offers), based on
88 customers (special terms), up to full blown calculation modules.
89
90 On a third front sales personnel asked for ways to see what price options a
91 position in a quotation has, and wanted information available when a price
92 offer changed.
93
94 Price sources put that together by making some compromises:
95
96 =over 4
97
98 =item 1.
99
100 Only change the price on creation of a position or when asked to.
101
102 =item 2.
103
104 Either set the price from a price source and let it be read only, or use a free
105 price.
106
107 =item 3.
108
109 Save the origin of each price with the record so that the calculation can be
110 reproduced.
111
112 =item 4.
113
114 Make price calculation flexible and pluggable.
115
116 =back
117
118 The first point creates user security by never changing a price for them
119 without their explicit consent, eliminating all problems originating from
120 trying to be smart. The second and third one ensure that later on the
121 calculation can be repeated so that invalid prices can be caught (because for
122 example the special offer is no longer valid), and so that sales personnel have
123 information about rising or falling prices. The fourth point ensures that
124 insular calculation processes can be developed independent of the core code.
125
126 =head1 INTERFACE METHODS
127
128 =over 4
129
130 =item C<new PARAMS>
131
132 C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
133 not have to be registered in C<record>.
134
135 =item C<price_from_source>
136
137 Attempts to retrieve a formerly calculated price with the same conditions
138
139 =item C<available_prices>
140
141 Returns all available prices.
142
143 =item C<best_price>
144
145 Attempts to get the best available price. returns L<empty_price> if no price is found.
146
147 =item C<empty_price>
148
149 A special empty price, that does not change the previously entered price, and
150 opens the price field to manual changes.
151
152 =back
153
154 =head1 SEE ALSO
155
156 L<SL::PriceSource::Base>,
157 L<SL::PriceSource::Price>,
158 L<SL::PriceSource::ALL>
159
160 =head1 BUGS AND CAVEATS
161
162 =over 4
163
164 =item *
165
166 The current simple model of price sources providing a simple value in simple
167 cases doesn't work well in situations where prices are modified by other
168 properties. The same problem also causes headaches when trying to use price
169 sources to compute positions in assemblies.
170
171 The solution should be to split price sources in simple ones, which do not
172 manage their interactions with record_items, but can be used in contexts
173 without record_items, and complex ones which do, but have to be fed a dummy
174 record_item. For the former there should be a wrapper that handles interactions
175 with units, price_factors etc..
176
177 =item *
178
179 Currently it is only possible to provide additional prices, but not to restrict
180 prices. Potential scenarios include credit limit customers which do not receive
181 benefits from sales, or general ALLOW, DENY order calculation.
182
183 =back
184
185 =head1 AUTHOR
186
187 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
188
189 =cut