--- /dev/null
+package SL::Controller::PriceRule;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ParseFilter;
+use SL::Controller::Helper::ReportGenerator;
+use SL::DB::PriceRule;
+use SL::DB::PriceRuleItem;
+use SL::DB::Pricegroup;
+use SL::DB::PartsGroup;
+use SL::DB::Business;
+use SL::Helper::Flash;
+use SL::ClientJS;
+use SL::Locale::String;
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(models price_rule vc js pricegroups partsgroups businesses) ],
+);
+
+# __PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('add_javascripts');
+
+#
+# actions
+#
+
+sub action_list {
+ my ($self) = @_;
+
+ $self->make_filter_summary;
+
+ my $price_rules = $self->models->get;
+
+ $self->prepare_report;
+
+ $self->report_generator_list_objects(report => $self->{report}, objects => $price_rules);
+}
+
+sub action_new {
+ my ($self) = @_;
+
+ $self->price_rule(SL::DB::PriceRule->new);
+ $self->price_rule->assign_attributes(%{ $::form->{price_rule} || {} });
+ $self->display_form;
+}
+
+sub action_edit {
+ my ($self) = @_;
+
+ $self->display_form;
+}
+
+sub action_create {
+ my ($self) = @_;
+
+ $self->price_rule($self->price_rule->clone_and_reset_deep);
+ $self->create_or_update;
+}
+
+sub action_update {
+ my ($self) = @_;
+ $self->create_or_update;
+}
+
+ #TODO
+sub action_destroy {
+ my ($self) = @_;
+
+ $self->price_rule->obsolete(1);
+ $self->price_rule->save;
+ flash_later('info', $::locale->text('The price rule has been obsoleted.'));
+
+ $self->redirect_to(action => 'list');
+}
+
+sub action_add_item_row {
+ my ($self, %params) = @_;
+
+ my $item = SL::DB::PriceRuleItem->new(type => $::form->{type});
+
+ my $html = $self->render('price_rule/item', { output => 0 }, item => $item);
+
+ $self
+ ->js
+ ->before('#price_rule_new_items', $html)
+ ->reinit_widgets
+ ->render($self);
+}
+
+#
+# filters
+#
+
+sub check_auth {
+ $::auth->assert('price_rule_edit');
+}
+
+#
+# helpers
+#
+
+sub display_form {
+ my ($self, %params) = @_;
+ my $is_new = !$self->price_rule->id;
+ my $title = $is_new ? t8('Create a new price rule') : t8('Edit price rule');
+ $self->render('price_rule/form',
+ title => $title,
+ %params
+ );
+}
+
+sub create_or_update {
+ my $self = shift;
+ my $is_new = !$self->price_rule->id;
+ my $params = delete($::form->{price_rule}) || { };
+
+ delete $params->{id};
+ $self->price_rule->assign_attributes(%{ $params });
+
+ my @errors = $self->price_rule->validate;
+
+ if (@errors) {
+ flash('error', @errors);
+ $self->display_form(callback => $::form->{callback});
+ return;
+ }
+
+ $self->price_rule->save;
+
+ flash_later('info', $is_new ? $::locale->text('The price rule has been created.') : $::locale->text('The price rule has been saved.'));
+
+ $self->redirect_to($::form->{callback} || (action => 'list', 'filter.type' => $self->price_rule->type));
+}
+
+sub prepare_report {
+ my ($self) = @_;
+
+ my $callback = $self->models->get_callback;
+
+ my $report = SL::ReportGenerator->new(\%::myconfig, $::form);
+ $self->{report} = $report;
+
+ my @columns = qw(name type priority price discount);
+ my @sortable = qw(name type priority price 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 } },
+ price => { sub => sub { $_[0]->price_as_number } },
+ discount => { sub => sub { $_[0]->discount_as_number } },
+ obsolete => { sub => sub { $_[0]->obsolete_as_bool_yn } },
+ );
+
+ map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
+
+ if ( $report->{options}{output_format} =~ /^(pdf|csv)$/i ) {
+ $self->models->disable_plugin('paginated');
+ }
+ $report->set_options(
+ std_column_visibility => 1,
+ controller_class => 'PriceRule',
+ output_format => 'HTML',
+ title => $::locale->text('Price Rules'),
+ allow_pdf_export => 1,
+ allow_csv_export => 1,
+ );
+ $report->set_columns(%column_defs);
+ $report->set_column_order(@columns);
+ $report->set_export_options(qw(list filter));
+ $report->set_options_from_form;
+ $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
+ $report->set_options(
+ raw_bottom_info_text => $self->render('price_rule/report_bottom', { output => 0 }),
+ raw_top_info_text => $self->render('price_rule/report_top', { output => 0 }),
+ );
+}
+
+sub make_filter_summary {
+ my ($self) = @_;
+
+ my $filter = $::form->{filter} || {};
+ my @filter_strings;
+
+ my @filters = (
+ [ $filter->{"name:substr::ilike"}, t8('Name') ],
+ [ $filter->{"price:number"}, t8('Price') ],
+ [ $filter->{"discount:number"}, t8('Discount') ],
+ );
+
+ for (@filters) {
+ push @filter_strings, "$_->[1]: $_->[0]" if $_->[0];
+ }
+
+ $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub all_price_rule_item_types {
+ SL::DB::Manager::PriceRuleItem->get_all_types($_[0]->price_rule->type);
+}
+
+sub add_javascripts {
+ $::request->{layout}->add_javascripts(qw(kivi.PriceRule.js autocomplete_customer.js autocomplete_vendor.js));
+}
+
+sub init_price_rule {
+ my ($self) = @_;
+
+ my $price_rule = $::form->{price_rule}{id} ? SL::DB::PriceRule->new(id => $::form->{price_rule}{id})->load : SL::DB::PriceRule->new;
+
+ my $items = delete $::form->{price_rule}{items};
+
+ $price_rule->assign_attributes(%{ $::form->{price_rule} || {} });
+
+ my %old_items = map { $_->id => $_ } $price_rule->items;
+
+ my @items;
+ for my $raw_item (@$items) {
+ my $item = $raw_item->{id} ? $old_items{ $raw_item->{id} } || SL::DB::PriceRuleItem->new(id => $raw_item->{id})->load : SL::DB::PriceRuleItem->new;
+ $item->assign_attributes(%$raw_item);
+ push @items, $item;
+ }
+
+ $price_rule->items(@items) if @items;
+
+ $self->price_rule($price_rule);
+}
+
+sub init_vc {
+ $::form->{filter}{type};
+}
+
+sub init_js {
+ SL::ClientJS->new;
+}
+
+sub init_businesses {
+ SL::DB::Manager::Business->get_all;
+}
+
+sub init_pricegroups {
+ SL::DB::Manager::Pricegroup->get_all;
+}
+
+sub init_partsgroups {
+ SL::DB::Manager::PartsGroup->get_all;
+}
+
+sub init_models {
+ my ($self) = @_;
+
+ SL::Controller::Helper::GetModels->new(
+ controller => $self,
+ sorted => {
+ name => t8('Name'),
+ type => t8('Type'),
+ priority => t8('Priority'),
+ price => t8('Price'),
+ discount => t8('Discount'),
+ obsolete => t8('Obsolete'),
+ },
+ );
+}
+
+1;
use SL::DB::PriceFactor;
use SL::DB::Pricegroup;
use SL::DB::Price;
+use SL::DB::PriceRule;
+use SL::DB::PriceRuleItem;
use SL::DB::Printer;
use SL::DB::Project;
use SL::DB::ProjectParticipant;
periodic_invoices_configs => 'periodic_invoices_config',
prices => 'price',
price_factors => 'price_factor',
+ price_rules => 'price_rule',
+ price_rule_items => 'price_rule_item',
pricegroup => 'pricegroup',
printers => 'printer',
project => 'project',
--- /dev/null
+# This file as been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::PriceRule;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Filtered;
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+use SL::DBUtils;
+
+use SL::Locale::String qw(t8);
+
+sub object_class { 'SL::DB::PriceRule' }
+
+__PACKAGE__->make_manager_methods;
+
+sub get_matching_filter {
+ my ($class, %params) = @_;
+
+ die 'need record' unless $params{record};
+ die 'need record_item' unless $params{record_item};
+
+ my $type = $params{record}->is_sales ? 'customer' : 'vendor';
+
+ # plan: 1. search all rule_items that do NOT match this record/record item combo
+ my ($sub_where, @value_subs) = SL::DB::Manager::PriceRuleItem->not_matching_sql_and_values(type => $type);
+ my @values = map { $_->($params{record}, $params{record_item}) } @value_subs;
+
+ # now union all NOT matching, invert ids, load these
+ my $matching_rule_ids = <<SQL;
+ SELECT id FROM price_rules
+ WHERE id NOT IN (
+ SELECT price_rules_id FROM price_rule_items WHERE $sub_where
+ )
+ AND type = ? AND NOT obsolete
+SQL
+
+ push @values, $type;
+
+ return $matching_rule_ids, @values;
+}
+
+sub get_all_matching {
+ my ($self, %params) = @_;
+
+ my ($query, @values) = $self->get_matching_filter(%params);
+ my @ids = selectall_ids($::form, $::form->get_standard_dbh, $query, 0, @values);
+
+ $self->get_all(query => [ id => \@ids ]);
+}
+
+sub _sort_spec {
+ return ( columns => { SIMPLE => 'ALL', },
+ default => [ 'name', 1 ],
+ nulls => { price => 'LAST', discount => 'LAST' }
+ );
+}
+
+1;
--- /dev/null
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::Manager::PriceRuleItem;
+
+use strict;
+
+use SL::DB::Helper::Manager;
+use base qw(SL::DB::Helper::Manager);
+
+sub object_class { 'SL::DB::PriceRuleItem' }
+
+__PACKAGE__->make_manager_methods;
+
+use SL::Locale::String qw(t8);
+
+my @types = qw(
+ customer vendor business partsgroup qty reqdate pricegroup
+);
+
+my %ops = (
+ 'num' => { eq => '=', lt => '<', gt => '>' },
+ 'date' => { eq => '=', lt => '<', gt => '>' },
+);
+
+my %types = (
+ 'customer' => { description => t8('Customer'), customer => 1, vendor => 0, data_type => 'int', data => sub { $_[0]->customer->id }, },
+ 'vendor' => { description => t8('Vendor'), customer => 0, vendor => 1, data_type => 'int', data => sub { $_[0]->vendor->id }, },
+ 'business' => { description => t8('Type of Business'), customer => 1, vendor => 1, data_type => 'int', data => sub { $_[0]->customervendor->business_id }, },
+ 'reqdate' => { description => t8('Reqdate'), customer => 1, vendor => 1, data_type => 'date', data => sub { $_[0]->reqdate }, ops => 'date' },
+ 'pricegroup' => { description => t8('Pricegroup'), customer => 1, vendor => 1, data_type => 'int', data => sub { $_[1]->pricegroup_id }, },
+ 'partsgroup' => { description => t8('Group'), customer => 1, vendor => 1, data_type => 'int', data => sub { $_[1]->part->partsgroup_id }, },
+ 'qty' => { description => t8('Qty'), customer => 1, vendor => 1, data_type => 'num', data => sub { $_[1]->qty }, ops => 'num' },
+);
+
+sub not_matching_sql_and_values {
+ my ($class, %params) = @_;
+
+ die 'must be called with a customer/vendor type' unless $params{type};
+
+ my (@tokens, @values);
+
+ for my $type (@types) {
+ my $def = $types{$type};
+ next unless $def->{$params{type}};
+
+ if ($def->{ops}) {
+ my $ops = $ops{$def->{ops}};
+
+ my @sub_tokens;
+ for (keys %$ops) {
+ push @sub_tokens, "op = '$_' AND NOT value_$def->{data_type} $ops->{$_} ?";
+ push @values, $def->{data};
+ }
+
+ push @tokens, "type = '$type' AND " . join ' OR ', map "($_)", @sub_tokens;
+ } else {
+ push @tokens, "type = '$type' AND NOT value_$def->{data_type} = ?";
+ push @values, $def->{data};
+ }
+ }
+
+ return join(' OR ', map "($_)", @tokens), @values;
+}
+
+sub get_all_types {
+ my ($class, $vc) = @_;
+
+ [ map { [ $_, $types{$_}{description} ] } grep { $types{$_}{$vc} } map { $_ } @types ];
+}
+
+1;
--- /dev/null
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::PriceRule;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__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' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
--- /dev/null
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::PriceRuleItem;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('price_rule_items');
+
+__PACKAGE__->meta->columns(
+ custom_variable_configs_id => { type => 'integer' },
+ id => { type => 'serial', not_null => 1 },
+ op => { type => 'text' },
+ price_rules_id => { type => 'integer', not_null => 1 },
+ type => { type => 'text' },
+ value_date => { type => 'date' },
+ value_int => { type => 'integer' },
+ value_num => { type => 'numeric', precision => 15, scale => 5 },
+ value_text => { type => 'text' },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+__PACKAGE__->meta->foreign_keys(
+ custom_variable_configs => {
+ class => 'SL::DB::CustomVariableConfig',
+ key_columns => { custom_variable_configs_id => 'id' },
+ },
+
+ price_rules => {
+ class => 'SL::DB::PriceRule',
+ key_columns => { price_rules_id => 'id' },
+ },
+);
+
+1;
+;
return $self->${ \ $number_method{$self->type} }(@_);
}
+sub customervendor {
+ $_[0]->is_sales ? $_[0]->customer : $_[0]->vendor;
+}
+
sub date {
goto &transdate;
}
--- /dev/null
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::PriceRule;
+
+use strict;
+
+use SL::DB::MetaSetup::PriceRule;
+use SL::DB::Manager::PriceRule;
+use Rose::DB::Object::Helpers qw(clone_and_reset);
+use SL::Locale::String qw(t8);
+
+__PACKAGE__->meta->add_relationship(
+ items => {
+ type => 'one to many',
+ class => 'SL::DB::PriceRuleItem',
+ column_map => { id => 'price_rules_id' },
+ },
+);
+
+__PACKAGE__->meta->initialize;
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(price_or_discount_state) ],
+);
+
+sub match {
+ my ($self, %params) = @_;
+
+ die 'need record' unless $params{record};
+ die 'need record_item' unless $params{record_item};
+
+ for ($self->items) {
+ next if $_->match(%params);
+ # TODO save for error
+ return
+ }
+
+ return 1;
+}
+
+sub is_sales {
+ $_[0]->type eq 'customer' ? 1
+ : $_[0]->type eq 'vendor' ? 0 : do { die 'wrong type' };
+}
+
+sub price_or_discount {
+ my ($self, $value) = @_;
+
+ if (@_ > 1) {
+ my $number = $self->price || $self->discount;
+ if ($value) {
+ $self->discount($number);
+ } else {
+ $self->price($number);
+ }
+ $self->price_or_discount_state($value);
+ }
+ $self->price_or_discount_state;
+}
+
+sub price_or_discount_as_number {
+ my ($self, @slurp) = @_;
+
+ $self->price_or_discount ? $self->price(undef) : $self->discount(undef);
+ $self->price_or_discount ? $self->discount_as_number(@slurp) : $self->price_as_number(@slurp);
+}
+
+sub init_price_or_discount_state {
+ defined $_[0]->price ? 0
+ : defined $_[0]->discount ? 1 : 0
+}
+
+sub validate {
+ my ($self) = @_;
+
+ 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;
+
+ return @errors;
+}
+
+sub clone_and_reset_deep {
+ my ($self) = @_;
+
+ my $clone = $self->clone_and_reset;
+ $clone->items(map { $_->clone_and_reset } $self->items);
+ $clone->name('');
+
+ return $clone;
+}
+
+sub full_description {
+ my ($self) = @_;
+
+ my $items = join ', ', map { $_->full_description } $self->items;
+ my $price = $self->price_or_discount
+ ? t8('Discount #1%', $self->discount_as_number)
+ : t8('Price #1', $self->price_as_number);
+
+ sprintf "%s: %s (%s)", $self->name, $price, $items;
+}
+
+sub in_use {
+ my ($self) = @_;
+
+ # is use is in this case used by record_items for their current price source
+ # so, get any of those that might have it
+ require SL::DB::OrderItem;
+ require SL::DB::DeliveryOrderItem;
+ require SL::DB::InvoiceItem;
+
+ my $price_source_spec = 'price_rules' . '/' . $self->id;
+
+ SL::DB::Manager::OrderItem->get_all_count(query => [ active_price_source => $price_source_spec ])
+ || SL::DB::Manager::DeliveryOrderItem->get_all_count(query => [ active_price_source => $price_source_spec ])
+ || SL::DB::Manager::InvoiceItem->get_all_count(query => [ active_price_source => $price_source_spec ]);
+}
+
+
+1;
--- /dev/null
+# This file has been auto-generated only because it didn't exist.
+# Feel free to modify it at will; it will not be overwritten automatically.
+
+package SL::DB::PriceRuleItem;
+
+use strict;
+
+use SL::DB::MetaSetup::PriceRuleItem;
+use SL::DB::Manager::PriceRuleItem;
+use Rose::DB::Object::Helpers qw(clone_and_reset);
+use SL::Locale::String qw(t8);
+
+__PACKAGE__->meta->initialize;
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(object operator) ],
+);
+
+sub match {
+ my ($self, %params) = @_;
+
+ die 'need record' unless $params{record};
+ die 'need record_item' unless $params{record_item};
+
+ $self->${\ "match_" . $self->type }(%params);
+}
+
+sub match_customer {
+ $_[0]->value_int == $_[1]{record}->customer_id;
+}
+sub match_vendor {
+ $_[0]->value_int == $_[1]{record}->vendor_id;
+}
+sub match_business {
+ $_[0]->value_int == $_[1]{record}->customervendor->business_id;
+}
+sub match_partsgroup {
+ $_[0]->value_int == $_[1]{record_item}->parts->partsgroup_id;
+}
+sub match_qty {
+ if ($_[0]->op eq 'eq') {
+ return $_[0]->value_num == $_[1]{record_item}->qty
+ } elsif ($_[0]->op eq 'lt') {
+ return $_[0]->value_num < $_[1]{record_item}->qty;
+ } elsif ($_[0]->op eq 'gt') {
+ return $_[0]->value_num > $_[1]{record_item}->qty;
+ }
+}
+sub match_reqdate {
+ if ($_[0]->op eq 'eq') {
+ return $_[0]->value_date == $_[1]{record}->reqdate;
+ } elsif ($_[0]->op eq 'lt') {
+ return $_[0]->value_date < $_[1]{record}->reqdate;
+ } elsif ($_[0]->op eq 'gt') {
+ return $_[0]->value_date > $_[1]{record}->reqdate;
+ }
+}
+sub match_pricegroup {
+ $_[0]->value_int == $_[1]{record_item}->customervendor->pricegroup_id;
+}
+
+sub customer {
+ require SL::DB::Customer;
+ SL::DB::Customer->load_cached($_[0]->value_int);
+}
+
+sub vendor {
+ require SL::DB::Vendor;
+ SL::DB::Vendor->load_cached($_[0]->value_int);
+}
+
+sub business {
+ require SL::DB::Business;
+ SL::DB::Business->load_cached($_[0]->value_int);
+}
+
+sub partsgroup {
+ require SL::DB::PartsGroup;
+ SL::DB::PartsGroup->load_cached($_[0]->value_int);
+}
+
+sub pricegroup {
+ require SL::DB::Pricegroup;
+ SL::DB::Pricegroup->load_cached($_[0]->value_int);
+}
+
+sub full_description {
+ my ($self) = @_;
+
+ my $type = $self->type;
+ my $op = $self->op;
+
+ $type eq 'customer' ? t8('Customer') . ' ' . $self->customer->displayable_name
+ : $type eq 'vendor' ? t8('Vendor') . ' ' . $self->vendor->displayable_name
+ : $type eq 'business' ? t8('Type of Business') . ' ' . $self->business->displayable_name
+ : $type eq 'partsgroup' ? t8('Group') . ' ' . $self->partsgroup->displayable_name
+ : $type eq 'pricegroup' ? t8('Pricegroup') . ' ' . $self->pricegroup->displayable_name
+ : $type eq 'qty' ? (
+ $op eq 'eq' ? t8('Qty equals #1', $self->value_num_as_number)
+ : $op eq 'lt' ? t8('Qty less than #1', $self->value_num_as_number)
+ : $op eq 'gt' ? t8('Qty more than #1', $self->value_num_as_number)
+ : do { die "unknown op $op for type $type" } )
+ : $type eq 'reqdate' ? (
+ $op eq 'eq' ? t8('Reqdate is #1', $self->value_date_as_date)
+ : $op eq 'lt' ? t8('Reqdate is before #1', $self->value_date_as_date)
+ : $op eq 'gt' ? t8('Reqdate is after #1', $self->value_date_as_date)
+ : do { die "unknown op $op for type $type" } )
+ : do { die "unknown type $type" }
+}
+
+1;
use SL::PriceSource::Customer;
use SL::PriceSource::Vendor;
use SL::PriceSource::Business;
+use SL::PriceSource::PriceRules;
my %price_sources_by_name = (
master_data => 'SL::PriceSource::MasterData',
pricegroup => 'SL::PriceSource::Pricegroup',
makemodel => 'SL::PriceSource::Makemodel',
business => 'SL::PriceSource::Business',
+ price_rules => 'SL::PriceSource::PriceRules',
);
my @price_sources_order = qw(
pricegroup
makemodel
business
+ price_rules
);
sub all_enabled_price_sources {
--- /dev/null
+package SL::PriceSource::PriceRules;
+
+use strict;
+use parent qw(SL::PriceSource::Base);
+
+use SL::PriceSource::Price;
+use SL::Locale::String;
+use SL::DB::PriceRule;
+use List::UtilsBy qw(min_by max_by);
+
+sub name { 'price_rules' }
+
+sub description { t8('Price Rules') }
+
+sub available_rules {
+ my ($self, %params) = @_;
+
+ SL::DB::Manager::PriceRule->get_all_matching(record => $self->record, record_item => $self->record_item);
+}
+
+sub available_prices {
+ my ($self, %params) = @_;
+
+ my $rules = $self->available_rules;
+
+ map { $self->make_price_from_rule($_) } @$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);
+}
+
+sub best_price {
+ my ($self) = @_;
+
+ $self->make_price_from_rule( min_by { $self->price_for_rule($_) } max_by { $_->priority } @{ $self->available_rules });
+}
+
+sub price_for_rule {
+ my ($self, $rule) = @_;
+ $rule->price_or_discount
+ ? (1 - $rule->discount / 100) * ($rule->is_sales ? $self->part->sellprice : $self->part->lastcost)
+ : $_->price;
+}
+
+sub make_price_from_rule {
+ my ($self, $rule) = @_;
+
+ SL::PriceSource::Price->new(
+ price => $self->price_for_rule($rule),
+ spec => $rule->id,
+ description => $rule->name,
+ price_source => $self,
+ )
+}
+
+1;
--- /dev/null
+namespace('kivi.PriceRule', function(ns) {
+
+ ns.add_new_row = function (type) {
+ var data = {
+ action: 'PriceRule/add_item_row',
+ type: type
+ };
+ $.post('controller.pl', data, kivi.eval_json_result);
+ }
+
+ $(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();
+ })
+ });
+});
'Add links' => 'Verknüpfungen hinzufügen',
'Add new currency' => 'Neue Währung hinzufügen',
'Add new custom variable' => 'Neue benutzerdefinierte Variable erfassen',
+ 'Add new price rule item' => 'Neue Bedingung hinzufügen',
'Add note' => 'Notiz erfassen',
'Add part' => 'Artikel hinzufügen',
'Add picture' => 'Bild hinzufügen',
'Create a new group' => 'Neue Benutzergruppe erfassen',
'Create a new payment term' => 'Neue Zahlungsbedingungen anlegen',
'Create a new predefined text' => 'Einen neuen vordefinierten Textblock anlegen',
+ 'Create a new price rule' => 'Neue Preisregel anlegen',
'Create a new printer' => 'Einen neuen Drucker anlegen',
'Create a new project' => 'Neues Projekt anlegen',
'Create a new project and link to it.' => 'Neues Projekt anlegen und damit verknüpfen.',
'Discard duplicate entries in CSV file' => 'Doppelte Einträge in CSV-Datei verwerfen',
'Discard entries with duplicates in database or CSV file' => 'Einträge aus CSV-Datei verwerfen, die es bereits in der Datenbank oder der CSV-Datei gibt',
'Discount' => 'Rabatt',
+ 'Discount #1%' => 'Rabatt #1%',
'Discounts' => 'Rabatte',
'Display' => 'Anzeigen',
'Display file' => 'Datei anzeigen',
'Edit payment term' => 'Zahlungsbedingungen bearbeiten',
'Edit picture' => 'Bild bearbeiten',
'Edit predefined text' => 'Vordefinierten Textblock bearbeiten',
+ 'Edit price rule' => 'Preisregel bearbeiten',
'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)',
'Edit project' => 'Projekt bearbeiten',
'Edit project #1' => 'Projekt #1 bearbeiten',
'II' => 'II',
'III' => 'III',
'IV' => 'IV',
+ 'If all of the following match' => 'Wenn alle der folgenden Bedingungen zutreffen',
'If amounts differ more than "Maximal amount difference" (see settings), this item is marked as invalid.' => 'Weichen die Beträge mehr als die "maximale Betragsabweichung" (siehe Einstellungen) ab, so wird diese Position als ungültig markiert.',
'If checked the taxkey will not be exported in the DATEV Export, but only IF chart taxkeys differ from general ledger taxkeys' => 'Falls angehakt wird der DATEV-Steuerschlüssel bei Buchungen auf dieses Konto nicht beim DATEV-Export mitexportiert, allerdings nur wenn zusätzlich der Konto-Steuerschlüssel vom Buchungs (Hauptbuch) Steuerschlüssel abweicht',
'If configured this bin will be preselected for all new parts. Also this bin will be used as the master default bin, if default transfer out with master bin is activated.' => 'Falls konfiguriert, wird dieses Lager mit Lagerplatz für neu angelegte Waren vorausgewählt.',
'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
'Netto Terms' => 'Zahlungsziel netto',
'New Password' => 'Neues Passwort',
+ 'New Price Rule' => 'Neue Preisregel',
'New assembly' => 'Neues Erzeugnis',
'New bank account' => 'Neues Bankkonto',
'New client #1: The database configuration fields "host", "port", "name" and "user" must not be empty.' => 'Neuer Mandant #1: Die Datenbankkonfigurationsfelder "Host", "Port" und "Name" dürfen nicht leer sein.',
'Previous transdate text' => 'wurde gespeichert am',
'Previous transnumber text' => 'Letzte Buchung mit der Buchungsnummer',
'Price' => 'Preis',
+ 'Price #1' => 'Preis #1',
'Price Factor' => 'Preisfaktor',
'Price Factors' => 'Preisfaktoren',
+ 'Price Rules' => 'Preisregeln',
'Price Source' => 'Preisquelle',
'Price Sources to be disabled in this client' => 'Preisquellen die in diesem Mandanten deaktiviert werden sollen',
'Price factor (database ID)' => 'Preisfaktor (Datenbank-ID)',
'Price group (database ID)' => 'Preisgruppe (Datenbank-ID)',
'Price group (name)' => 'Preisgruppe (Name) ',
'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',
'Pricegroup' => 'Preisgruppe',
'Pricegroup deleted!' => 'Preisgruppe gelöscht!',
'Printer management' => 'Druckerverwaltung',
'Printing ... ' => 'Es wird gedruckt.',
'Prior year' => 'Vorheriges Jahr',
+ 'Priority' => 'Priorität',
'Private E-mail' => 'Private E-Mail',
'Private Phone' => 'Privates Tel.',
'Problem' => 'Problem',
'Projects' => 'Projekte',
'Projecttransactions' => 'Projektbuchungen',
'Prozentual/Absolut' => 'Prozentual/Absolut',
+ 'Purchase' => '',
'Purchase Delivery Orders' => 'Einkaufslieferscheine',
'Purchase Delivery Orders deleteable' => 'Einkaufslieferscheine löschbar',
'Purchase Invoice' => 'Einkaufsrechnung',
'Purchase Orders' => 'Lieferantenaufträge',
'Purchase Orders deleteable' => 'Lieferantenaufträge löschbar',
'Purchase Price' => 'Einkaufspreis',
+ 'Purchase Price Rules ' => 'Preisregeln (Einkauf)',
'Purchase Prices' => 'Einkaufspreise',
'Purchase delivery order' => 'Lieferschein (Einkauf)',
'Purchase invoices' => 'Einkaufsrechnungen',
'Purpose' => 'Verwendungszweck',
'Qty' => 'Menge',
'Qty according to delivery order' => 'Menge laut Lieferschein',
+ 'Qty equals #1' => 'Menge ist #1',
'Qty in Selected Records' => 'Menge in gewählten Belegen',
'Qty in stock' => 'Lagerbestand',
+ 'Qty less than #1' => 'Menge weniger als #1',
+ 'Qty more than #1' => 'Menge mehr als #1',
'Quantity' => 'Menge',
'Quantity missing.' => 'Die Mengenangabe fehlt.',
'Quartal' => 'Quartal',
'Representative' => 'Vertreter',
'Representative for Customer' => 'Vertreter für Kunden',
'Reqdate' => 'Liefertermin',
+ 'Reqdate is #1' => 'Liefertermin ist #1',
+ 'Reqdate is after #1' => 'Liefertermin nach #1',
+ 'Reqdate is before #1' => 'Liefertermin vor #1',
'Reqdate not set or before current month' => 'Lieferdatum nicht gesetzt oder vor aktuellem Monat',
'Request Quotations' => 'Preisanfragen',
'Request for Quotation' => 'Anfrage',
'Saldo neu' => 'Saldo neu',
'Saldo per' => 'Saldo per',
'Sale Prices' => 'Verkaufspreise',
+ 'Sales' => '',
'Sales Delivery Orders' => 'Verkaufslieferscheine',
'Sales Delivery Orders deleteable' => 'Verkaufslieferscheine löschbar',
'Sales Invoice' => 'Rechnung',
'Sales Order' => 'Kundenauftrag',
'Sales Orders' => 'Aufträge',
'Sales Orders deleteable' => 'Kundenaufträge löschbar',
+ 'Sales Price Rules ' => 'Preisregeln (Verkauf)',
'Sales Price information' => 'Verkaufspreisinformation',
'Sales Quotation valid interval' => 'Angebotsgültigkeitsintervall',
'Sales Quotations' => 'Angebote',
'Service Number missing!' => 'Dienstleistungsnummer fehlt!',
'Service, assembly or part' => 'Dienstleistung, Erzeugnis oder Ware',
'Services' => 'Dienstleistungen',
+ 'Set (set to)' => 'Setze',
'Set eMail text' => 'E-Mail Text eingeben',
'Settings' => 'Einstellungen',
'Setup Menu' => 'Menü-Variante',
'The name is missing in row %d.' => 'Der Name fehlt in Zeile %d.',
'The name is missing.' => 'Der Name fehlt.',
'The name is not unique.' => 'Der Name ist nicht eindeutig.',
+ 'The name must not be empty.' => 'Der Name darf nicht leer sein.',
'The name must only consist of letters, numbers and underscores and start with a letter.' => 'Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.',
'The new requirement spec template will be a copy of \'#1\'.' => 'Die neue Pflichtenheftvorlage wird eine Kopie von \'#1\' sein.',
'The new requirement spec will be a copy of \'#1\' for customer \'#2\'.' => 'Das neue Pflichtenheft wird eine Kopie von \'#1\' für Kunde \'#2\' sein.',
'The predefined text has been saved.' => 'Der vordefinierte Textblock wurde gespeichert.',
'The predefined text is in use and cannot be deleted.' => 'Der vordefinierte Textblock wird verwendet und kann nicht gelöscht werden.',
'The preferred one is to install packages provided by your operating system distribution (e.g. Debian or RPM packages).' => 'Die bevorzugte Art, ein Perl-Modul zu installieren, ist durch Installation eines von Ihrem Betriebssystem zur Verfügung gestellten Paketes (z.B. Debian-Pakete oder RPM).',
+ 'The price rule has been created.' => 'Die Preisregel wurde angelegt.',
+ 'The price rule has been obsoleted.' => 'Diese Preisregel ist nicht mehr gültig',
+ 'The price rule has been saved.' => 'Die preisregel wurde gespeichert.',
'The printer could not be deleted.' => 'Der Drucker konnte nicht gelöscht werden.',
'The printer has been created.' => 'Der Drucker wurde angelegt.',
'The printer has been deleted.' => 'Der Drucker wurde entfernt.',
'The wrong taxkeys for AP and AR transactions have been fixed.' => 'Die Probleme mit falschen Steuerschlüssel bei Kreditoren- und Debitorenbuchungen wurden behoben.',
'The wrong taxkeys for inventory transactions for sales and purchase invoices have been fixed.' => 'Die falschen Steuerschlüssel für Warenbestandsbuchungen bei Einkaufs- und Verkaufsrechnungen wurden behoben.',
'The wrong taxkeys have been fixed.' => 'Die Steuerschlüssel wurden nach Ihrer Auswahl korrigiert.',
+ 'Then' => 'Dann',
'Then go to the database administration and chose "create database".' => 'Dann gehen Sie in den Datenbankverwaltung, und wählen Sie dort "Datenbank anlegen" aus.',
'There are #1 more open invoices for this customer with other currencies.' => 'Es gibt #1 weitere offene Rechnungen für diesen Kunden, die in anderen Währungen ausgestellt wurden.',
'There are #1 more open invoices from this vendor with other currencies.' => 'Es gibt #1 weitere offene Rechnungen von diesem Lieferanten, die in anderen Währungen ausgestellt wurden.',
'invoice' => 'Rechnung',
'invoice mode or item mode' => 'Rechnungsmodus oder Artikelmodus',
'invoice_list' => 'debitorenbuchungsliste',
+ 'is' => 'ist',
+ 'is after' => 'ist nach dem',
+ 'is before' => 'ist vor dem',
+ 'is equal to' => 'ist gleich',
+ 'is greater than' => 'ist größer als',
+ 'is lower than' => 'ist kleiner als',
'kivitendo' => 'kivitendo',
'kivitendo Homepage' => 'Infos zu kivitendo',
'kivitendo can fix these problems automatically.' => 'kivitendo kann solche Probleme automatisch beheben.',
'terminated' => 'gekündigt',
'time and effort based position' => 'Aufwandsposition',
'to (date)' => 'bis',
+ 'to (set to)' => 'auf',
'to (time)' => 'bis',
'transfer' => 'Umlagerung',
'transferred in' => 'eingelagert',
module=ic.pl
action=search_update_prices
+[Master Data--Sales Price Rules ]
+ACCESS=part_service_assembly_edit
+module=controller.pl
+action=PriceRule/list
+filter.type=customer
+
+[Master Data--Purchase Price Rules ]
+ACCESS=part_service_assembly_edit
+module=controller.pl
+action=PriceRule/list
+filter.type=vendor
[Master Data--Reports]
module=menu.pl
--- /dev/null
+-- @tag: price_rules
+-- @description: Preismatrix Tabellen
+-- @depends: release_3_1_0
+-- @encoding: utf-8
+
+CREATE TABLE price_rules (
+ id SERIAL PRIMARY KEY,
+ name TEXT,
+ type TEXT,
+ priority INTEGER NOT NULL DEFAULT 3,
+ price NUMERIC(15,5),
+ discount NUMERIC(15,5),
+ obsolete BOOLEAN NOT NULL DEFAULT FALSE,
+ itime TIMESTAMP,
+ mtime TIMESTAMP
+);
+
+CREATE TABLE price_rule_items (
+ id SERIAL PRIMARY KEY,
+ price_rules_id INTEGER NOT NULL,
+ type TEXT,
+ op TEXT,
+ custom_variable_configs_id INTEGER,
+ value_text TEXT,
+ value_int INTEGER,
+ value_date DATE,
+ value_num NUMERIC(15,5),
+ itime TIMESTAMP,
+ mtime TIMESTAMP,
+ FOREIGN KEY (price_rules_id) REFERENCES price_rules (id),
+ FOREIGN KEY (custom_variable_configs_id) REFERENCES custom_variable_configs (id)
+);
+
+CREATE TRIGGER mtime_price_rules BEFORE UPDATE ON price_rules FOR EACH ROW EXECUTE PROCEDURE set_mtime();
+CREATE TRIGGER mtime_price_rule_items BEFORE UPDATE ON price_rule_items FOR EACH ROW EXECUTE PROCEDURE set_mtime();
--- /dev/null
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+ [% SELF.filter_summary | html %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+ <table id='filter_table'>
+ <tr>
+ <tr>
+ <th align="right">[% 'Description' | $T8 %]</th>
+ <td>[% L.input_tag('filter.name:substr::ilike', filter.name_substr__ilike, size = 20) %]</td>
+ </tr>
+ <tr>
+ <th align="right">[% 'Price' | $T8 %]</th>
+ <td>[% L.input_tag('filter.price:number', filter.price_number, size=20) %]</td>
+ </tr>
+ <tr>
+ <th align="right">[% 'Discount' | $T8 %]</th>
+ <td>[% L.input_tag('filter.discount:number', filter.discount_number, size=20) %]</td>
+ </tr>
+ </table>
+
+[% L.hidden_tag('action', 'PriceRule/dispatch') %]
+[% L.hidden_tag('filter.type', FORM.filter.type) %]
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
+
+<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table input[type=checkbox]").prop("checked", 0);'>[% 'Reset' | $T8 %]</a>
+
+</div>
+
+</form>
--- /dev/null
+[%- USE L %]
+[%- USE T8 %]
+[% L.select_tag('', SELF.all_price_rule_item_types, id='price_rules_empty_item_select') %]
+<a id='price_rule_item_add'>[% 'Add new price rule item' | $T8 %]</a>
--- /dev/null
+[%- USE T8 %]
+[%- USE L %][%- USE P %]
+[%- USE HTML %][%- USE LxERP %]
+
+<h1>[% title %]</h1>
+
+[%- INCLUDE 'common/flash.html' %]
+
+ <form method="post" action="controller.pl">
+ [% L.hidden_tag("price_rule.id", SELF.price_rule.id) %]
+ [% L.hidden_tag("price_rule.type", SELF.price_rule.type) %]
+
+ <table>
+ <tr>
+ <th align="right">[% 'Name' | $T8 %]</th>
+ <td>[% L.input_tag("price_rule.name", SELF.price_rule.name, size=60, class='initial_focus') %]</td>
+ </tr>
+[%- IF 0 %]
+ <tr>
+ <th align="right">[% 'Type' | $T8 %]</th>
+ <td>[% L.select_tag("price_rule.type", [ [ 'sales', LxERP.t8('Sales')], [ 'purchase', LxERP.t8('Purchase') ] ], default=SELF.price_rule.type) %]</td>
+ </tr>
+[%- END %]
+ <tr>
+ <th align="right">[% 'Priority' | $T8 %]</th>
+ <td>[% L.select_tag('price_rule.priority', [1,2,3,4,5], default=SELF.price_rule.priority, style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% 'Valid' | $T8 %]</th>
+ <td>[% L.select_tag('project.project_type_id', [ [ 0, LxERP.t8('Valid') ], [ 1 , LxERP.t8('Obsolete')]], default=SELF.price_rule.obsolete, title_key='description', style='width: 300px') %]</td>
+ </tr>
+
+ <tr>
+ <tr>
+ </table>
+
+<h3>[% 'If all of the following match' | $T8 %]:</h3>
+
+<div id='price_rule_items' style='margin-left: 20px;'>
+ [% FOREACH item = SELF.price_rule.items %]
+ [% PROCESS 'price_rule/item.html' item=item %]
+ [% END %]
+ <div id='price_rule_new_items'></div>
+ <div>[% PROCESS 'price_rule/empty_item.html' %]</div>
+</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>
+
+ <p>
+ [% L.hidden_tag("action", "PriceRule/dispatch") %]
+ [% L.hidden_tag("callback", FORM.callback) %]
+ [% L.submit_tag("action_" _ (SELF.price_rule.id ? "update" : "create"), LxERP.t8('Save')) %]
+ [%- IF SELF.price_rule.id %]
+ [% L.submit_tag("action_create", LxERP.t8('Save as new')) %]
+ [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) IF !SELF.price_rule.in_use %]
+ [%- END %]
+ <a href="[% SELF.url_for(action='list', 'vc'=SELF.price_rule.type) %]">[%- LxERP.t8('Abort') %]</a>
+ </p>
+ </form>
--- /dev/null
+[%- USE L %]
+[%- USE HTML %]
+[%- USE T8 %]
+[%- USE LxERP %]
+[% SET num_compare_ops = [
+ [ 'eq', LxERP.t8('is equal to') ],
+ [ 'lt', LxERP.t8('is lower than') ],
+ [ 'gt', LxERP.t8('is greater than') ],
+] %]
+[% SET date_compare_ops = [
+ [ 'eq', LxERP.t8('is equal to') ],
+ [ 'gt', LxERP.t8('is after') ],
+ [ 'lt', LxERP.t8('is before') ],
+] %]
+<div>
+<a class='price_rule_remove_line'><img height="10px" width="10px" src="image/cross.png" alt="[% 'Remove' | $T8 %]"></a>
+[% L.hidden_tag('price_rule.items[+].id', item.id) %]
+[% L.hidden_tag('price_rule.items[].type', item.type) %]
+[%- SWITCH item.type %]
+ [% CASE 'customer' %]
+ [% 'Customer' | $T8 %] [% 'is' | $T8 %] [% L.customer_vendor_picker('price_rule.items[].value_int', item.customer, type='customer') %]
+ [% CASE 'vendor' %]
+ [% 'Vendor' | $T8 %] [% 'is' | $T8 %] [% L.vendor_vendor_picker('price_rule.items[].value_int', item.customer, type='vendor') %]
+ [% CASE 'business' %]
+ [% 'Type of Business' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.businesses, title_key='description', default=item.value_int) %]
+ [% CASE 'partsgroup' %]
+ [% 'Group' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.partsgroups, title_key='partsgroup', default=item.value_int) %]
+ [% CASE 'qty' %]
+ [% 'Quantity' | $T8 %] [% L.select_tag('price_rule.items[].op', num_compare_ops, default=item.op) %] [% L.input_tag('price_rule.items[].value_num_as_number', item.value_num_as_number) %]
+ [% CASE 'reqdate' %]
+ [% 'Reqdate' | $T8 %] [% L.select_tag('price_rule.items[].op', date_compare_ops, default=item.op) %] [% L.date_tag('price_rule.items[].value_date', item.value_date) %]
+ [% CASE 'pricegroup' %]
+ [% 'Pricegroup' | $T8 %] [% 'is' | $T8 %] [% L.select_tag('price_rule.items[].value_int', SELF.pricegroups, title_key='pricegroup', default=item.value_int) %]
+ [% CASE %]
+[%- END %]
+</div>
--- /dev/null
+[% USE L %]
+[% USE T8 %]
+[% USE HTML %]
+[%- L.paginate_controls(models=SELF.models) %]
+
+<a href="[% SELF.url_for(action='new', 'price_rule.type'=SELF.vc, callback=SELF.models.get_callback) | html %]">[% 'New Price Rule' | $T8 %]</a>
--- /dev/null
+[%- USE L %]
+[%- PROCESS 'price_rule/_filter.html' filter=SELF.filter %]
+ <hr>