PriceSource: editable Dokumentation
[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 discount_from_source {
35   my ($self, $source) = @_;
36   my ($source_name, $spec) = split m{/}, $source, 2;
37
38   my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
39
40   return $class
41     ? $class->new(record_item => $self->record_item, record => $self->record)->discount_from_source($source, $spec)
42     : empty_discount();
43 }
44
45 sub available_prices {
46   map { $_->available_prices } $_[0]->all_price_sources;
47 }
48
49 sub available_discounts {
50   return if $_[0]->record_item->part->not_discountable;
51   map { $_->available_discounts } $_[0]->all_price_sources;
52 }
53
54 sub best_price {
55   min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
56 }
57
58 sub best_discount {
59   max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
60 }
61
62 sub empty_price {
63   SL::PriceSource::Price->new(
64     description => t8('None (PriceSource)'),
65   );
66 }
67
68 sub empty_discount {
69   SL::PriceSource::Discount->new(
70     description => t8('None (PriceSource Discount)'),
71   );
72 }
73
74 1;
75
76 __END__
77
78 =encoding utf-8
79
80 =head1 NAME
81
82 SL::PriceSource - mixin for price_sources in record items
83
84 =head1 DESCRIPTION
85
86 PriceSource is an interface that allows generic algorithms to be plugged
87 together to calculate available prices for a position in a record.
88
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.
91
92 =head1 BACKGROUND AND PHILOSOPHY
93
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.
97
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.
102
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.
105
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.
109
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.
113
114 Price sources now extend the previous pricing by attaching a source to every
115 price in records. The information it provides are:
116
117 =over 4
118
119 =item 1.
120
121 Where did this price originate?
122
123 =item 2.
124
125 If this price would be calculated today, is it still the same as it was when
126 this record was created?
127
128 =item 3.
129
130 If I want to price an item in this record now, which prices are available?
131
132 =item 4.
133
134 Which one is the "best"?
135
136 =back
137
138 =head1 GUARANTEES
139
140 To ensure price source prices are comprehensible and reproducible, some
141 invariants are guaranteed:
142
143 =over 4
144
145 =item 1.
146
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.
149
150 =item 2.
151
152 If a price is set from a source then the system will try to prevent the user
153 from messing it up. By default this means the price will be read-only.
154 Implementations can choose to make prices editable, but even then deviations
155 from the calculatied price will be marked.
156
157 A that is not set from a source will not have any of this.
158
159 =item 3.
160
161 A price should be able to repeat the calculations done to arrive at the price
162 when it was first used. If these calculations are no longer applicable (special
163 offer expired) this should be signalled. If the calculations result in a
164 different price, this should be signalled. If the calculations fail (needed
165 information is no longer present) this must be signalled.
166
167 =back
168
169 The first point creates user security by never changing a price for them
170 without their explicit consent, eliminating all problems originating from
171 trying to be smart. The second and third one ensure that later on the
172 calculation can be repeated so that invalid prices can be caught (because for
173 example the special offer is no longer valid), and so that sales personnel have
174 information about rising or falling prices.
175
176 =head1 STRUCTURE
177
178 Price sources are managed by this package (L<SL::PriceSource>), and all
179 external access should be by using it's interface.
180
181 Each source is an instance of L<SL::PriceSource::Base> and the available
182 implementations are recorded in L<SL::PriceSource::ALL>. Prices and discounts
183 returned by interface methods are instances of L<SL::PriceSource::Price> and
184 L<SL::PriceSource::Discout>.
185
186 Returned prices and discounts should be checked for entries in C<invalid> and
187 C<missing>, see documentation in their classes.
188
189 =head1 INTERFACE METHODS
190
191 =over 4
192
193 =item C<new PARAMS>
194
195 C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
196 not have to be registered in C<record>.
197
198 =item C<price_from_source>
199
200 Attempts to retrieve a formerly calculated price with the same conditions
201
202 =item C<discount_from_source>
203
204 Attempts to retrieve a formerly calculated discount with the same conditions
205
206 =item C<available_prices>
207
208 Returns all available prices.
209
210 =item C<available_discounts>
211
212 Returns all available discounts.
213
214 =item C<best_price>
215
216 Attempts to get the best available price. returns L<empty_price> if no price is found.
217
218 =item C<best_discount>
219
220 Attempts to get the best available discount. returns L<empty_discount> if no discount is found.
221
222 =item C<empty_price>
223
224 A special empty price, that does not change the previously entered price, and
225 opens the price field to manual changes.
226
227 =item C<empty_discount>
228
229 A special empty discount, that does not change the previously entered discount, and
230 opens the discount field to manual changes.
231
232 =back
233
234 =head1 SEE ALSO
235
236 L<SL::PriceSource::Base>,
237 L<SL::PriceSource::Price>,
238 L<SL::PriceSource::Discount>,
239 L<SL::PriceSource::ALL>
240
241 =head1 BUGS AND CAVEATS
242
243 =over 4
244
245 =item *
246
247 The current model of price sources requires a record and a record_item for
248 every price calculation. This means that price structures can never be used
249 when no record is available, such as calculation the worth of assembly rows.
250
251 A possible solution is to either split price sources into simple and complex
252 ones (where the former do not require records).
253
254 Another would be to have default values for the input normally taken from
255 records (like qty defaulting to 1).
256
257 A last one would be to provide an alternative input channel for needed
258 properties.
259
260 =item *
261
262 Discount sources were implemented as a copy of the prices with slightly
263 different semantics. Need to do a real design. A requirement is, that a single
264 source can provide both prices and discounts (needed for price_rules).
265
266 =item *
267
268 Priorities are implemented ad hoc. The semantics which are chosen by the "best"
269 accessors are unintuitive because they do not guarantee anything. Better
270 terminology might help.
271
272 =item *
273
274 It is currently not possible to link a price to the price of the generating
275 record_item (i.e. the price of a delivery order item to the order item it was
276 generated from). This is crucial to enterprises that calculate all their prices
277 in orders, and update those after they made delivery orders.
278
279 =item *
280
281 Currently it is only possible to provide additional prices, but not to restrict
282 prices. Potential scenarios include credit limit customers which do not receive
283 benefits from sales, or general ALLOW, DENY order calculation.
284
285 =item *
286
287 Composing price sources is disallowed for clarity, but all price sources need
288 to be aware of units and price_factors. This is madness.
289
290 =item *
291
292 The current implementation of lastcost is useless. Since it's one of the
293 master_data prices it will always compete with listprice. But in real scenarios
294 the listprice tends to go up, while lastcost stays the same, so lastcost
295 usually wins. Lastcost could be lower priority, but a better design would be
296 nice.
297
298 =item *
299
300 Guarantee 1 states that price sources will never change prices on their own.
301 Further testing in the wild has shown that this is desirable within a record,
302 but not when copying items from one record to another within a workflow.
303
304 Specifically when changing from sales to purchase records prices don't make
305 sense anymore. The guarantees should be updated to reflect this and
306 transposition guidelines should be documented.
307
308 =item *
309
310 Prices were originally planned as a context element rather than a modal popup.
311 It would be great to have this now with better framework.
312
313 =back
314
315 =head1 AUTHOR
316
317 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
318
319 =cut