epic-s6ts
[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 fast) ],
7   'scalar --get_set_init' => [ qw(
8     best_price best_discount
9   ) ],
10   'array --get_set_init' => [ qw(
11     all_price_sources
12     available_prices available_discounts
13   ) ],
14 );
15
16 use List::UtilsBy qw(min_by max_by);
17 use SL::PriceSource::ALL;
18 use SL::PriceSource::Price;
19 use SL::Locale::String;
20
21 sub init_all_price_sources {
22   my ($self) = @_;
23
24   [ map {
25     $self->price_source_by_class($_);
26   } SL::PriceSource::ALL->all_enabled_price_sources ]
27 }
28
29 sub price_source_by_class {
30   my ($self, $class) = @_;
31   return unless $class;
32
33   $self->{price_source_by_name}{$class} //=
34     $class->new(record_item => $self->record_item, record => $self->record, fast => $self->fast);
35 }
36
37 sub price_from_source {
38   my ($self, $source) = @_;
39   return empty_price() if !$source;
40
41   ${ $self->{price_from_source} //= {} }{$source} //= do {
42     my ($source_name, $spec) = split m{/}, $source, 2;
43     my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
44     my $source_object = $self->price_source_by_class($class);
45
46     $source_object
47       ? $source_object->price_from_source($source, $spec)
48       : empty_price();
49   }
50 }
51
52 sub discount_from_source {
53   my ($self, $source) = @_;
54   return empty_discount() if !$source;
55
56   ${ $self->{discount_from_source} //= {} }{$source} //= do {
57     my ($source_name, $spec) = split m{/}, $source, 2;
58     my $class = SL::PriceSource::ALL->price_source_class_by_name($source_name);
59     my $source_object = $self->price_source_by_class($class);
60
61     $source_object
62       ? $source_object->discount_from_source($source, $spec)
63       : empty_discount();
64   }
65 }
66
67 sub init_available_prices {
68   [ map { $_->available_prices } $_[0]->all_price_sources ];
69 }
70
71 sub init_available_discounts {
72   return [] if $_[0]->record_item->part->not_discountable;
73   [ map { $_->available_discounts } $_[0]->all_price_sources ];
74 }
75
76 sub init_best_price {
77   min_by { $_->price } max_by { $_->priority } grep { $_->price > 0 } grep { $_ } map { $_->best_price } $_[0]->all_price_sources;
78 }
79
80 sub init_best_discount {
81   max_by { $_->discount } max_by { $_->priority } grep { $_->discount } grep { $_ } map { $_->best_discount } $_[0]->all_price_sources;
82 }
83
84 sub empty_price {
85   SL::PriceSource::Price->new(
86     description => t8('None (PriceSource)'),
87   );
88 }
89
90 sub empty_discount {
91   SL::PriceSource::Discount->new(
92     description => t8('None (PriceSource Discount)'),
93   );
94 }
95
96 1;
97
98 __END__
99
100 =encoding utf-8
101
102 =head1 NAME
103
104 SL::PriceSource - mixin for price_sources in record items
105
106 =head1 DESCRIPTION
107
108 PriceSource is an interface that allows generic algorithms to be plugged
109 together to calculate available prices for a position in a record.
110
111 Each algorithm can access details of the record to realize dependencies on
112 part, customer, vendor, date, quantity etc, which was previously not possible.
113
114 =head1 BACKGROUND AND PHILOSOPHY
115
116 sql ledger and subsequently Lx-Office had three prices per part: sellprice,
117 listprice and lastcost. When adding an item to a record, the applicable price
118 was copied and after that it was free to be changed.
119
120 Later on additional things were added. Various types of discount, vendor pricelists
121 and the infamous price groups. The problem was not that those didn't work, the
122 problem was they had to guess too much when to change a price with the
123 available price from the database, and when to leave the user entered price.
124
125 The result was that the price of an item in a record seemed to change on a
126 whim, and the origin of the price itself being opaque.
127
128 Unrelated to that, users asked for more ways to store special prices, based on
129 qty (block pricing, bulk discount), based on date (special offers), based on
130 customers (special terms), up to full blown calculation modules.
131
132 On a third front sales personnel asked for ways to see what price options a
133 position in a quotation has, and wanted information available when prices
134 changed to make better informed choices about sales later in the workflow.
135
136 Price sources now extend the previous pricing by attaching a source to every
137 price in records. The information it provides are:
138
139 =over 4
140
141 =item 1.
142
143 Where did this price originate?
144
145 =item 2.
146
147 If this price would be calculated today, is it still the same as it was when
148 this record was created?
149
150 =item 3.
151
152 If I want to price an item in this record now, which prices are available?
153
154 =item 4.
155
156 Which one is the "best"?
157
158 =back
159
160 =head1 GUARANTEES
161
162 To ensure price source prices are comprehensible and reproducible, some
163 invariants are guaranteed:
164
165 =over 4
166
167 =item 1.
168
169 Price sources will never on their own change a price. They will offer options,
170 and it is up to the user to change a price.
171
172 =item 2.
173
174 If a price is set from a source then the system will try to prevent the user
175 from messing it up. By default this means the price will be read-only.
176 Implementations can choose to make prices editable, but even then deviations
177 from the calculatied price will be marked.
178
179 A price that is not set from a source will not have any of this.
180
181 =item 3.
182
183 A price should be able to repeat the calculations done to arrive at the price
184 when it was first used. If these calculations are no longer applicable (special
185 offer expired) this should be signalled. If the calculations result in a
186 different price, this should be signalled. If the calculations fail (needed
187 information is no longer present) this must be signalled.
188
189 =back
190
191 The first point creates user security by never changing a price for them
192 without their explicit consent, eliminating all problems originating from
193 trying to be smart. The second and third one ensure that later on the
194 calculation can be repeated so that invalid prices can be caught (because for
195 example the special offer is no longer valid), and so that sales personnel have
196 information about rising or falling prices.
197
198 =head1 STRUCTURE
199
200 Price sources are managed by this package (L<SL::PriceSource>), and all
201 external access should be by using its interface.
202
203 Each source is an instance of L<SL::PriceSource::Base> and the available
204 implementations are recorded in L<SL::PriceSource::ALL>. Prices and discounts
205 returned by interface methods are instances of L<SL::PriceSource::Price> and
206 L<SL::PriceSource::Discount>.
207
208 Returned prices and discounts should be checked for entries in C<invalid> and
209 C<missing>, see documentation in their classes.
210
211 =head1 INTERFACE METHODS
212
213 =over 4
214
215 =item C<new PARAMS>
216
217 C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
218 not have to be registered in C<record>.
219
220 =item C<price_from_source>
221
222 Attempts to retrieve a formerly calculated price with the same conditions
223
224 =item C<discount_from_source>
225
226 Attempts to retrieve a formerly calculated discount with the same conditions
227
228 =item C<available_prices>
229
230 Returns all available prices.
231
232 =item C<available_discounts>
233
234 Returns all available discounts.
235
236 =item C<best_price>
237
238 Attempts to get the best available price. returns L<empty_price> if no price is
239 found.
240
241 =item C<best_discount>
242
243 Attempts to get the best available discount. returns L<empty_discount> if no
244 discount is found.
245
246 =item C<empty_price>
247
248 A special empty price that does not change the previously entered price and
249 opens the price field to manual changes.
250
251 =item C<empty_discount>
252
253 A special empty discount that does not change the previously entered discount
254 and opens the discount field to manual changes.
255
256 =item C<fast>
257
258 If set to true, indicates that calls may skip doing intensive work and instead
259 return a price or discount flagged as unknown. The caller must be prepared to
260 deal with those.
261
262 Typically this is intended to delay expensive calculations until they can be
263 done in a second batch pass. If the information is already present, it is still
264 encouraged that implementations return the correct values.
265
266 =back
267
268
269 =head1 SEE ALSO
270
271 L<SL::PriceSource::Base>,
272 L<SL::PriceSource::Price>,
273 L<SL::PriceSource::Discount>,
274 L<SL::PriceSource::ALL>
275
276 =head1 BUGS AND CAVEATS
277
278 =over 4
279
280 =item *
281
282 The current model of price sources requires a record and a record_item for
283 every price calculation. This means that price structures can never be used
284 when no record is available, such as calculation the worth of assembly rows.
285
286 A possible solution is to either split price sources into simple and complex
287 ones (where the former do not require records).
288
289 Another would be to have default values for the input normally taken from
290 records (like qty defaulting to 1).
291
292 A last one would be to provide an alternative input channel for needed
293 properties.
294
295 =item *
296
297 Discount sources were implemented as a copy of the prices with slightly
298 different semantics. Need to do a real design. A requirement is, that a single
299 source can provide both prices and discounts (needed for price_rules).
300
301 =item *
302
303 Priorities are implemented ad hoc. The semantics which are chosen by the "best"
304 accessors are unintuitive because they do not guarantee anything. Better
305 terminology might help.
306
307 =item *
308
309 It is currently not possible to link a price to the price of the generating
310 record_item (i.e. the price of a delivery order item to the order item it was
311 generated from). This is crucial to enterprises that calculate all their prices
312 in orders, and update those after they made delivery orders.
313
314 =item *
315
316 Currently it is only possible to provide additional prices, but not to restrict
317 prices. Potential scenarios include credit limit customers which do not receive
318 benefits from sales, or general ALLOW, DENY order calculation.
319
320 =item *
321
322 Composing price sources is disallowed for clarity, but all price sources need
323 to be aware of units and price_factors. This is madness.
324
325 =item *
326
327 The current implementation of lastcost is useless. Since it's one of the
328 master_data prices it will always compete with listprice. But in real scenarios
329 the listprice tends to go up, while lastcost stays the same, so lastcost
330 usually wins. Lastcost could be lower priority, but a better design would be
331 nice.
332
333 =item *
334
335 Guarantee 1 states that price sources will never change prices on their own.
336 Further testing in the wild has shown that this is desirable within a record,
337 but not when copying items from one record to another within a workflow.
338
339 Specifically when changing from sales to purchase records prices don't make
340 sense anymore. The guarantees should be updated to reflect this and
341 transposition guidelines should be documented.
342
343 The previously mentioned linked prices can emulated by allowing price sources
344 to set a new price when changing to a new record in the workflow. The decision
345 about whether a price is eligable to be set can be suggested by the price
346 source implementation but is ultimately up to the surrounding framework, which
347 can make this configurable.
348
349 =item *
350
351 Prices were originally planned as a context element rather than a modal popup.
352 It would be great to have this now with better framework.
353
354 =item *
355
356 Large records (30 positions or more) in combination with complicated price
357 sources run into n+1 problems. There should be an extra hook that allows price
358 source implementations to make bulk calculations before the actual position loop.
359
360 =item *
361
362 Prices have defined information channels for missing and invalid, but it would
363 be deriable to have more information flow. For example a limited offer might
364 expire in three days while the record is valid for 20 days. THis mismatch is
365 impossible to resolve automatically, but informing the user about it would be a
366 nice thing.
367
368 This can also extend to diagnostics on class level, where implementations can
369 call attention to likely misconfigurations.
370
371 =back
372
373 =head1 AUTHOR
374
375 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
376
377 =cut