Revision bc8c26f3
Von Sven Schöling vor etwa 10 Jahren hinzugefügt
SL/Controller/PriceRule.pm | ||
---|---|---|
->render($self);
|
||
}
|
||
|
||
sub action_price_type_help {
|
||
$_[0]->render('price_rule/price_type_help', { layout => 0 });
|
||
}
|
||
|
||
#
|
||
# filters
|
||
#
|
||
... | ... | |
my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
|
||
$self->{report} = $report;
|
||
|
||
my @columns = qw(name type priority price discount items);
|
||
my @sortable = qw(name type priority price discount );
|
||
my @columns = qw(name type priority price reduction discount items);
|
||
my @sortable = qw(name type priority price reduction discount );
|
||
|
||
my %column_defs = (
|
||
name => { obj_link => sub { $self->url_for(action => 'edit', 'price_rule.id' => $_[0]->id, callback => $callback) } },
|
||
priority => { sub => sub { $_[0]->priority_as_text } },
|
||
price => { sub => sub { $_[0]->price_as_number } },
|
||
reduction => { sub => sub { $_[0]->reduction_as_number } },
|
||
discount => { sub => sub { $_[0]->discount_as_number } },
|
||
obsolete => { sub => sub { $_[0]->obsolete_as_bool_yn } },
|
||
items => { sub => sub { $_[0]->item_summary } },
|
||
... | ... | |
SL::DB::Manager::PartsGroup->get_all;
|
||
}
|
||
|
||
sub all_price_types {
|
||
SL::DB::Manager::PriceRule->all_price_types;
|
||
}
|
||
|
||
sub init_models {
|
||
my ($self) = @_;
|
||
|
||
... | ... | |
priority => t8('Priority'),
|
||
price => t8('Price'),
|
||
discount => t8('Discount'),
|
||
reduction => t8('Reduced Master Data'),
|
||
obsolete => t8('Obsolete'),
|
||
items => t8('Rule Details'),
|
||
},
|
SL/DB/Manager/PriceRule.pm | ||
---|---|---|
|
||
use parent qw(SL::DB::Helper::Manager);
|
||
|
||
use constant PRICE_NEW => 0;
|
||
use constant PRICE_REDUCED_MASTER_DATA => 1;
|
||
use constant PRICE_DISCOUNT => 2;
|
||
|
||
use SL::DB::Helper::Filtered;
|
||
use SL::DB::Helper::Paginated;
|
||
use SL::DB::Helper::Sorted;
|
||
... | ... | |
$self->get_all(query => [ id => \@ids ]);
|
||
}
|
||
|
||
sub all_price_types {
|
||
[ PRICE_NEW, t8('Price') ],
|
||
[ PRICE_REDUCED_MASTER_DATA, t8('Reduced Master Data') ],
|
||
[ PRICE_DISCOUNT, t8('Discount') ],
|
||
}
|
||
|
||
sub _sort_spec {
|
||
return ( columns => { SIMPLE => 'ALL', },
|
||
default => [ 'name', 1 ],
|
SL/DB/MetaSetup/PriceRule.pm | ||
---|---|---|
__PACKAGE__->meta->table('price_rules');
|
||
|
||
__PACKAGE__->meta->columns(
|
||
discount => { type => 'numeric', precision => 15, scale => 5 },
|
||
id => { type => 'serial', not_null => 1 },
|
||
itime => { type => 'timestamp' },
|
||
mtime => { type => 'timestamp' },
|
||
name => { type => 'text' },
|
||
obsolete => { type => 'boolean', default => 'false', not_null => 1 },
|
||
price => { type => 'numeric', precision => 15, scale => 5 },
|
||
priority => { type => 'integer', default => 3, not_null => 1 },
|
||
type => { type => 'text' },
|
||
discount => { type => 'numeric', precision => 15, scale => 5 },
|
||
id => { type => 'serial', not_null => 1 },
|
||
itime => { type => 'timestamp' },
|
||
mtime => { type => 'timestamp' },
|
||
name => { type => 'text' },
|
||
obsolete => { type => 'boolean', default => 'false', not_null => 1 },
|
||
price => { type => 'numeric', precision => 15, scale => 5 },
|
||
priority => { type => 'integer', default => 3, not_null => 1 },
|
||
reduction => { type => 'numeric', precision => 15, scale => 5 },
|
||
type => { type => 'text' },
|
||
);
|
||
|
||
__PACKAGE__->meta->primary_key_columns([ 'id' ]);
|
SL/DB/PriceRule.pm | ||
---|---|---|
: $_[0]->type eq 'vendor' ? 0 : do { die 'wrong type' };
|
||
}
|
||
|
||
sub price_or_discount {
|
||
sub price_type {
|
||
my ($self, $value) = @_;
|
||
|
||
if (@_ > 1) {
|
||
my $number = $self->price || $self->discount;
|
||
if ($value) {
|
||
if ($value == SL::DB::Manager::PriceRule::PRICE_NEW()) {
|
||
$self->price($number);
|
||
} elsif ($value == SL::DB::Manager::PriceRule::PRICE_REDUCED_MASTER_DATA()) {
|
||
$self->reduction($number);
|
||
} elsif ($value == SL::DB::Manager::PriceRule::PRICE_DISCOUNT()) {
|
||
$self->discount($number);
|
||
} else {
|
||
$self->price($number);
|
||
die 'unknown price_or_discount value';
|
||
}
|
||
$self->price_or_discount_state($value);
|
||
}
|
||
... | ... | |
|
||
sub price_or_discount_as_number {
|
||
my ($self, @slurp) = @_;
|
||
my $type = $self->price_type;
|
||
|
||
$self->price(undef) unless $type == SL::DB::Manager::PriceRule::PRICE_NEW();
|
||
$self->reduction(undef) unless $type == SL::DB::Manager::PriceRule::PRICE_REDUCED_MASTER_DATA();
|
||
$self->discount(undef) unless $type == SL::DB::Manager::PriceRule::PRICE_DISCOUNT();
|
||
|
||
$self->price_or_discount ? $self->price(undef) : $self->discount(undef);
|
||
$self->price_or_discount ? $self->discount_as_number(@slurp) : $self->price_as_number(@slurp);
|
||
|
||
if ($type == SL::DB::Manager::PriceRule::PRICE_NEW()) {
|
||
return $self->price_as_number(@slurp)
|
||
} elsif ($type == SL::DB::Manager::PriceRule::PRICE_REDUCED_MASTER_DATA()) {
|
||
return $self->reduction_as_number(@slurp);
|
||
} elsif ($type == SL::DB::Manager::PriceRule::PRICE_DISCOUNT()) {
|
||
return $self->discount_as_number(@slurp)
|
||
} else {
|
||
die 'unknown price_or_discount';
|
||
}
|
||
}
|
||
|
||
sub init_price_or_discount_state {
|
||
defined $_[0]->price ? 0
|
||
: defined $_[0]->discount ? 1 : 0
|
||
defined $_[0]->price ? SL::DB::Manager::PriceRule::PRICE_NEW()
|
||
: defined $_[0]->reduction ? SL::DB::Manager::PriceRule::PRICE_REDUCED_MASTER_DATA()
|
||
: defined $_[0]->discount ? SL::DB::Manager::PriceRule::PRICE_DISCOUNT()
|
||
: SL::DB::Manager::PriceRule::PRICE_NEW();
|
||
}
|
||
|
||
sub validate {
|
||
... | ... | |
|
||
my @errors;
|
||
push @errors, $::locale->text('The name must not be empty.') if !$self->name;
|
||
push @errors, $::locale->text('Price or discount must not be zero.') if !$self->price && !$self->discount;
|
||
push @errors, $::locale->text('Price or discount must not be zero.') if !$self->price && !$self->discount && !$self->reduction;
|
||
push @errors, $::locale->text('Pirce rules must have at least one rule.') if !@{[ $self->items ]};
|
||
|
||
return @errors;
|
SL/PriceSource/PriceRules.pm | ||
---|---|---|
use parent qw(SL::PriceSource::Base);
|
||
|
||
use SL::PriceSource::Price;
|
||
use SL::PriceSource::Discount;
|
||
use SL::Locale::String;
|
||
use SL::DB::PriceRule;
|
||
use List::UtilsBy qw(min_by max_by);
|
||
... | ... | |
sub available_rules {
|
||
my ($self, %params) = @_;
|
||
|
||
SL::DB::Manager::PriceRule->get_all_matching(record => $self->record, record_item => $self->record_item);
|
||
$self->{available} ||= SL::DB::Manager::PriceRule->get_all_matching(record => $self->record, record_item => $self->record_item);
|
||
}
|
||
|
||
sub available_price_rules {
|
||
my $rules = $_[0]->available_rules;
|
||
grep { $_->price_type != SL::DB::Manager::PriceRule::PRICE_DISCOUNT() } @$rules
|
||
}
|
||
|
||
sub available_discount_rules {
|
||
my $rules = $_[0]->available_rules;
|
||
grep { $_->price_type == SL::DB::Manager::PriceRule::PRICE_DISCOUNT() } @$rules
|
||
}
|
||
|
||
sub available_prices {
|
||
my ($self, %params) = @_;
|
||
|
||
my $rules = $self->available_rules;
|
||
|
||
map { $self->make_price_from_rule($_) } @$rules;
|
||
map { $self->make_price_from_rule($_) } $self->available_price_rules;
|
||
}
|
||
|
||
sub available_discounts { }
|
||
sub available_discounts {
|
||
my ($self, %params) = @_;
|
||
|
||
map { $self->make_discount_from_rule($_) } $self->available_discount_rules;
|
||
}
|
||
|
||
sub price_from_source {
|
||
my ($self, $source, $spec) = @_;
|
||
|
||
my $rule = SL::DB::Manager::PriceRule->find_by(id => $spec);
|
||
$self->make_price_from_rule($rule);
|
||
if ($rule->price_type == SL::DB::Manager::PriceRule::PRICE_DISCOUNT()) {
|
||
return $self->make_discount_from_rule($rule);
|
||
} else {
|
||
return $self->make_price_from_rule($rule);
|
||
}
|
||
}
|
||
|
||
sub best_price {
|
||
my ($self) = @_;
|
||
|
||
my $rules = $self->available_rules;
|
||
my @rules = $self->available_price_rules;
|
||
|
||
return unless @$rules;
|
||
return unless @rules;
|
||
|
||
my @max_prio = max_by { $_->priority } @$rules;
|
||
my @max_prio = max_by { $_->priority } @rules;
|
||
my $min_price = min_by { $self->price_for_rule($_) } @max_prio;
|
||
|
||
$self->make_price_from_rule($min_price);
|
||
}
|
||
|
||
sub best_discount { }
|
||
sub best_discount {
|
||
my ($self) = @_;
|
||
|
||
my @rules = $self->available_discount_rules;
|
||
|
||
return unless @rules;
|
||
|
||
my @max_prio = max_by { $_->priority } @rules;
|
||
my $max_discount = max_by { $_->discount } @max_prio;
|
||
|
||
$self->make_discount_from_rule($max_discount);
|
||
}
|
||
|
||
sub price_for_rule {
|
||
my ($self, $rule) = @_;
|
||
$rule->price_or_discount
|
||
? (1 - $rule->discount / 100) * ($rule->is_sales ? $self->part->sellprice : $self->part->lastcost)
|
||
$rule->price_type != SL::DB::Manager::PriceRule::PRICE_NEW()
|
||
? (1 - $rule->reduction / 100) * ($rule->is_sales ? $self->part->sellprice : $self->part->lastcost)
|
||
: $rule->price;
|
||
}
|
||
|
||
... | ... | |
)
|
||
}
|
||
|
||
sub make_discount_from_rule {
|
||
my ($self, $rule) = @_;
|
||
|
||
SL::PriceSource::Discount->new(
|
||
discount => $rule->discount / 100,
|
||
spec => $rule->id,
|
||
description => $rule->name,
|
||
price_source => $self,
|
||
)
|
||
}
|
||
|
||
1;
|
js/kivi.PriceRule.js | ||
---|---|---|
$.post('controller.pl', data, kivi.eval_json_result);
|
||
}
|
||
|
||
ns.open_price_type_help_popup = function() {
|
||
kivi.popup_dialog({
|
||
url: 'controller.pl?action=PriceRule/price_type_help',
|
||
dialog: { title: kivi.t8('Price Types') },
|
||
});
|
||
}
|
||
|
||
$(function() {
|
||
$('#price_rule_item_add').click(function() {
|
||
ns.add_new_row($('#price_rules_empty_item_select').val());
|
||
... | ... | |
$('#price_rule_items').on('click', 'a.price_rule_remove_line', function(){
|
||
$(this).closest('div').remove();
|
||
})
|
||
$('#price_rule_price_type_help').click(ns.open_price_type_help_popup);
|
||
});
|
||
});
|
js/locale/de.js | ||
---|---|---|
"Part picker":"Artikelauswahl",
|
||
"Paste":"Einfügen",
|
||
"Paste template":"Vorlage einfügen",
|
||
"Price Types":"Preistypen",
|
||
"Project link actions":"Projektverknüpfungs-Aktionen",
|
||
"Quotations/Orders actions":"Aktionen für Angebote/Aufträge",
|
||
"Re-numbering all sections and function blocks in the order they are currently shown cannot be undone.":"Das Neu-Nummerieren aller Abschnitte und Funktionsblöcke kann nicht rückgängig gemacht werden.",
|
locale/de/all | ||
---|---|---|
'Content' => 'Inhalt',
|
||
'Continue' => 'Weiter',
|
||
'Contra' => 'gegen',
|
||
'Contrary to Reduced Master Data this will be shown as discount in records.' => 'Im Gegensatz zu Abschlag wird der Rabatt in Belegen ausgewiesen',
|
||
'Conversion of "birthday" contact person attribute' => 'Umstellung des Kontaktpersonenfeldes "Geburtstag"',
|
||
'Conversion to PDF failed: #1' => 'Konvertierung zu PDF schlug fehl: #1',
|
||
'Copies' => 'Kopien',
|
||
... | ... | |
'EuR' => 'EuR',
|
||
'Everyone can log in.' => 'Alle können sich anmelden.',
|
||
'Exact' => 'Genau',
|
||
'Example' => 'Beispiel',
|
||
'Example: http://kivitendo.de' => 'Beispiel: http://kivitendo.de',
|
||
'Excel' => 'Excel',
|
||
'Exch' => 'Wechselkurs.',
|
||
... | ... | |
'It is not allowed that a summary account occurs in a drop-down menu!' => 'Ein Sammelkonto darf nicht in Aufklappmenüs aufgenommen werden!',
|
||
'It is possible that even after such a correction there is something wrong with this transaction (e.g. taxes that don\'t match the selected taxkey). Therefore you should re-run the general ledger analysis.' => 'Auch nach einer Korrektur kann es mit dieser Buchung noch weitere Probleme geben (z.B. nicht zum Steuerschlüssel passende Steuern), weshalb ein erneutes Ausführen der Hauptbuchanalyse empfohlen wird.',
|
||
'It is possible to make a quick DATEV export everytime you post a record to ensure things work nicely with their data requirements. This will result in a slight overhead though you can enable this for each type of record independantly.' => 'Es ist möglich, bei jeder Buchung einen schnellen DATEV-Export durchzuführen, um sicherzustellen, dass die Datensätze den DATEV-Anforderungen genügen. Da dies einen kleinen Overhead bedeutet, lässt sich die Einstellung für jeden Buchungstyp getrennt einstellen.',
|
||
'It will not be further modified by any other source, and will be offered in records like this.' => 'Er wird nicht weiter verändert werden und genau so im Beleg vorgeschlagen werden.',
|
||
'It will simply set the taxkey to 0 (meaning "no taxes") which is the correct value for such inventory transactions.' => 'Es wird einfach die Steuerschlüssel auf 0 setzen, was "keine Steuer" bedeutet und für solche Warenbestandsbuchungen der richtige Wert ist.',
|
||
'Item deleted!' => 'Artikel gelöscht!',
|
||
'Item mode' => 'Artikelmodus',
|
||
... | ... | |
'Marked entries printed!' => 'Markierte Einträge wurden gedruckt!',
|
||
'Master Data' => 'Stammdaten',
|
||
'Master Data Bin Text Deleted' => 'Gelöschte Stammdaten Freitext-Lagerplätze',
|
||
'Matching Price Rules can apply in one of three types:' => 'Preisregeln können Preise in drei Varianten vorschlagen:',
|
||
'Max. Dunning Level' => 'höchste Mahnstufe',
|
||
'Maximal amount difference' => 'maximale Betragsabweichung',
|
||
'Maximum future booking interval' => 'Maximale Anzahl von Tagen an denen Buchungen in der Zukunft erlaubt sind.',
|
||
... | ... | |
'Name and Street' => 'Name und Straße',
|
||
'Name does not make sense without any bsooqr options' => 'Option "Name in gewählten Belegen" wird ignoriert.',
|
||
'Name in Selected Records' => 'Name in gewählten Belegen',
|
||
'Negative reductions are possible to model price increases.' => 'Negative Abschläge sind möglich um Aufschläge zu modellieren.',
|
||
'Neither sections nor function blocks have been created yet.' => 'Es wurden bisher weder Abschnitte noch Funktionsblöcke angelegt.',
|
||
'Net Income Statement' => 'Einnahmenüberschußrechnung',
|
||
'Net amount' => 'Nettobetrag',
|
||
... | ... | |
'Price Rules' => 'Preisregeln',
|
||
'Price Source' => 'Preisquelle',
|
||
'Price Sources to be disabled in this client' => 'Preisquellen die in diesem Mandanten deaktiviert werden sollen',
|
||
'Price Types' => 'Preistypen',
|
||
'Price factor (database ID)' => 'Preisfaktor (Datenbank-ID)',
|
||
'Price factor (name)' => 'Preisfaktor (Name)',
|
||
'Price factor deleted!' => 'Preisfaktor gelöscht.',
|
||
... | ... | |
'Price information' => 'Preisinformation',
|
||
'Price or discount must not be zero.' => 'Preis/Rabatt darf nicht 0,00 sein',
|
||
'Price sources deactivated in this client' => 'Preisquellen die in diesem Mandanten deaktiviert sind',
|
||
'Price type explanation' => 'Preistyp Erklärung',
|
||
'Pricegroup' => 'Preisgruppe',
|
||
'Pricegroup deleted!' => 'Preisgruppe gelöscht!',
|
||
'Pricegroup missing!' => 'Preisgruppe fehlt!',
|
||
... | ... | |
'Record type to create' => 'Anzulegender Belegtyp',
|
||
'Recorded Tax' => 'Gespeicherte Steuern',
|
||
'Recorded taxkey' => 'Gespeicherter Steuerschlüssel',
|
||
'Reduced Master Data' => 'Abschlag',
|
||
'Reference' => 'Referenz',
|
||
'Reference / Invoice Number' => 'Referenz / Rechnungsnummer',
|
||
'Reference day' => 'Stichtag',
|
||
... | ... | |
'The discount in percent' => 'Der prozentuale Rabatt',
|
||
'The discount must be less than 100%.' => 'Der Rabatt muss kleiner als 100% sein.',
|
||
'The discount must not be negative.' => 'Der Rabatt darf nicht negativ sein.',
|
||
'The discounted amount will be shown in documents.' => 'Der Rabattbetrag wird in Belegen ausgewiesen.',
|
||
'The dunning process started' => 'Der Mahnprozess ist gestartet.',
|
||
'The dunnings have been printed.' => 'Die Mahnung(en) wurden gedruckt.',
|
||
'The end date is the last day for which invoices will possibly be created.' => 'Das Enddatum ist das letztmögliche Datum, an dem eine Rechnung erzeugt wird.',
|
||
... | ... | |
'This user is a member in the following groups' => 'Dieser Benutzer ist Mitglied in den folgenden Gruppen',
|
||
'This user will have access to the following clients' => 'Dieser Benutzer wird Zugriff auf die folgenden Mandanten haben',
|
||
'This vendor number is already in use.' => 'Diese Lieferantennummer wird bereits verwendet.',
|
||
'This will apply a 3% reduction to the master data price before entering it into the record item.' => 'Diese Zeile zieht vom Stammdatenpreis 3% ab, und schlägt den resultierenden Preis vor.',
|
||
'This will be treated as a discount in percent points.' => 'Diese Option schlägt den Wert in Prozentpunkten als Rabatt vor.',
|
||
'This will happen before the price is offered, and the reduction will not be printed in documents.' => 'Das passiert, bevor der Preis vorgeschlagen wird, und der Abschlag wird nicht in Belegen ausgewiesen.',
|
||
'This will reduce the appropriate Master Data price by this in percent points.' => 'Diese Option reduziert den zugehörigen Stammdatenpreis um den angegebenen Wert in Prozentpunkten.',
|
||
'This will set an exact price.' => 'Diese Option setzt einen festen Preis.',
|
||
'Three Options:' => 'Drei Optionen:',
|
||
'Time Format' => 'Uhrzeitformat',
|
||
'Time Tracking' => 'Zeiterfassung',
|
sql/Pg-upgrade2/price_rules_discount.sql | ||
---|---|---|
-- @tag: price_rules_discount
|
||
-- @description: Preisregeln: Beim Löschen items mitlöschen
|
||
-- @depends: release_3_1_0 price_rules_cascade_delete
|
||
|
||
ALTER TABLE price_rules RENAME COLUMN discount TO reduction;
|
||
ALTER TABLE price_rules ADD COLUMN discount NUMERIC(15,5);
|
templates/webpages/price_rule/_filter.html | ||
---|---|---|
<th align="right">[% 'Price' | $T8 %]</th>
|
||
<td>[% L.input_tag('filter.price:number', filter.price_number, size=20, style='width: 300px') %]</td>
|
||
</tr>
|
||
<tr>
|
||
<th align="right">[% 'Reduced Master Data' | $T8 %]</th>
|
||
<td>[% L.input_tag('filter.reduction:number', filter.reduction_number, size=20, style='width: 300px') %]</td>
|
||
</tr>
|
||
<tr>
|
||
<th align="right">[% 'Discount' | $T8 %]</th>
|
||
<td>[% L.input_tag('filter.discount:number', filter.discount_number, size=20, style='width: 300px') %]</td>
|
templates/webpages/price_rule/form.html | ||
---|---|---|
</div>
|
||
|
||
<h3>[% 'Then' | $T8 %]:</h3>
|
||
<div>[% 'Set (set to)' | $T8 %] [% L.select_tag('price_rule.price_or_discount', [ [0, LxERP.t8('Price') ], [1, LxERP.t8('Discount') ]], default=SELF.price_rule.price_or_discount) %] [% 'to (set to)' | $T8 %] [% L.input_tag('price_rule.price_or_discount_as_number', SELF.price_rule.price_or_discount_as_number) %]
|
||
<div>[% 'Set (set to)' | $T8 %] [% L.select_tag('price_rule.price_type', SELF.all_price_types, default=SELF.price_rule.price_type) %] [% 'to (set to)' | $T8 %] [% L.input_tag('price_rule.price_or_discount_as_number', SELF.price_rule.price_or_discount_as_number) %] <a id='price_rule_price_type_help' title='[% 'Price type explanation' | $T8 %]'>[?]</a>
|
||
</div>
|
||
|
||
<p>
|
templates/webpages/price_rule/price_type_help.html | ||
---|---|---|
[% USE T8 %]
|
||
[% 'Matching Price Rules can apply in one of three types:' | $T8 %]
|
||
|
||
<h3>[% 'Price' | $T8 %]</h3>
|
||
|
||
<p>
|
||
[% 'This will set an exact price.' | $T8 %]
|
||
[% 'It will not be further modified by any other source, and will be offered in records like this.' | $T8 %]
|
||
</p>
|
||
|
||
<h3>[% 'Reduced Master Data' | $T8 %]</h3>
|
||
|
||
<p>
|
||
[% 'This will reduce the appropriate Master Data price by this in percent points.' | $T8 %]
|
||
[% 'This will happen before the price is offered, and the reduction will not be printed in documents.' | $T8 %]
|
||
[% 'Negative reductions are possible to model price increases.' | $T8 %]
|
||
</p>
|
||
|
||
<p>[% 'Example' | $T8 %]:</p>
|
||
|
||
<pre>[% 'Set (set to)' | $T8 %] [% 'Reduced Master Data' | $T8 %] [% 'to (set to)' | $T8 %] 3</pre>
|
||
|
||
<p>
|
||
[% 'This will apply a 3% reduction to the master data price before entering it into the record item.' | $T8 %]
|
||
</p>
|
||
|
||
<h3>[% 'Discount' | $T8 %]</h3>
|
||
|
||
<p>
|
||
[% 'This will be treated as a discount in percent points.' | $T8 %]
|
||
[% 'Contrary to Reduced Master Data this will be shown as discount in records.' | $T8 %]
|
||
[% 'The discounted amount will be shown in documents.' | $T8 %]
|
||
</p>
|
Auch abrufbar als: Unified diff
PriceRule: Preisregeln können jetzt auch Rabatte
ausserdem Doku