66daad285bdbce31dd0598338786ef0fd5de38a8
[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. At the moment a part is loaded into a record, the
96 applicable price is copied and after that it is 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 is not that those didn't work, the
100 problem is, that 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 Unrelated to that, users asked for more ways to store special prices, based on
104 qty (block pricing, bulk discount), based on date (special offers), based on
105 customers (special terms), up to full blown calculation modules.
106
107 On a third front sales personnel asked for ways to see what price options a
108 position in a quotation has, and wanted information available when a price
109 offer changed.
110
111 Price sources put that together by making some compromises:
112
113 =over 4
114
115 =item 1.
116
117 Only change the price on creation of a position or when asked to.
118
119 =item 2.
120
121 Either set the price from a price source and let it be read only, or use a free
122 price.
123
124 =item 3.
125
126 Save the origin of each price with the record so that the calculation can be
127 reproduced.
128
129 =item 4.
130
131 Make price calculation flexible and pluggable.
132
133 =back
134
135 The first point creates user security by never changing a price for them
136 without their explicit consent, eliminating all problems originating from
137 trying to be smart. The second and third one ensure that later on the
138 calculation can be repeated so that invalid prices can be caught (because for
139 example the special offer is no longer valid), and so that sales personnel have
140 information about rising or falling prices. The fourth point ensures that
141 insular calculation processes can be developed independent of the core code.
142
143 =head1 INTERFACE METHODS
144
145 =over 4
146
147 =item C<new PARAMS>
148
149 C<PARAMS> must contain both C<record> and C<record_item>. C<record_item> does
150 not have to be registered in C<record>.
151
152 =item C<price_from_source>
153
154 Attempts to retrieve a formerly calculated price with the same conditions
155
156 =item C<discount_from_source>
157
158 Attempts to retrieve a formerly calculated discount with the same conditions
159
160 =item C<available_prices>
161
162 Returns all available prices.
163
164 =item C<available_discounts>
165
166 Returns all available discounts.
167
168 =item C<best_price>
169
170 Attempts to get the best available price. returns L<empty_price> if no price is found.
171
172 =item C<best_discount>
173
174 Attempts to get the best available discount. returns L<empty_discount> if no discount is found.
175
176 =item C<empty_price>
177
178 A special empty price, that does not change the previously entered price, and
179 opens the price field to manual changes.
180
181 =item C<empty_discount>
182
183 A special empty discount, that does not change the previously entered discount, and
184 opens the discount field to manual changes.
185
186 =back
187
188 =head1 SEE ALSO
189
190 L<SL::PriceSource::Base>,
191 L<SL::PriceSource::Price>,
192 L<SL::PriceSource::Discount>,
193 L<SL::PriceSource::ALL>
194
195 =head1 BUGS AND CAVEATS
196
197 =over 4
198
199 =item *
200
201 The current simple model of price sources providing a simple value in simple
202 cases doesn't work well in situations where prices are modified by other
203 properties. The same problem also causes headaches when trying to use price
204 sources to compute positions in assemblies.
205
206 The solution should be to split price sources in simple ones, which do not
207 manage their interactions with record_items, but can be used in contexts
208 without record_items, and complex ones which do, but have to be fed a dummy
209 record_item. For the former there should be a wrapper that handles interactions
210 with units, price_factors etc..
211
212 =item *
213
214 Currently it is only possible to provide additional prices, but not to restrict
215 prices. Potential scenarios include credit limit customers which do not receive
216 benefits from sales, or general ALLOW, DENY order calculation.
217
218 =back
219
220 =head1 AUTHOR
221
222 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
223
224 =cut