site/cgi-bin/modules/BSE/TB/Orders.pm
site/cgi-bin/modules/BSE/TB/OwnedFile.pm
site/cgi-bin/modules/BSE/TB/OwnedFiles.pm
+site/cgi-bin/modules/BSE/TB/PriceTier.pm
+site/cgi-bin/modules/BSE/TB/PriceTierPrice.pm
+site/cgi-bin/modules/BSE/TB/PriceTierPrices.pm
+site/cgi-bin/modules/BSE/TB/PriceTiers.pm
site/cgi-bin/modules/BSE/TB/ProductOption.pm
site/cgi-bin/modules/BSE/TB/ProductOptions.pm
site/cgi-bin/modules/BSE/TB/ProductOptionValue.pm
display_order integer not null default -1,
unique only_one(owner_id, owner_type, file_id)
-) type = InnoDB;
\ No newline at end of file
+) type = InnoDB;
+
+drop table if exists bse_price_tiers;
+create table bse_price_tiers (
+ id integer not null auto_increment primary key,
+
+ description text not null,
+
+ group_id integer null,
+
+ from_date date null,
+ to_date date null,
+
+ display_order integer null null
+);
+
+drop table if exists bse_price_tier_prices;
+
+create table bse_price_tier_prices (
+ id integer not null auto_increment primary key,
+
+ tier_id integer not null,
+ product_id integer not null,
+
+ retailPrice integer not null,
+
+ unique tier_product(tier_id, product_id)
+);
+
# conditional in case something strange is in the config file
my $dynamic = $cfg->entry('basic', 'all_dynamic', 0) ? 1 : 0;
+ if (!$dynamic && $self->generator =~ /\bCatalog\b/) {
+ require Products;
+ my @tiers = Products->pricing_tiers;
+ @tiers and $dynamic = 1;
+ }
+
$dynamic or $dynamic = $self->{force_dynamic};
$dynamic or $dynamic = $self->is_access_controlled;
use DevHelp::Date qw(dh_parse_date dh_parse_sql_date);
use constant MAX_FILE_DISPLAYNAME_LENGTH => 255;
-our $VERSION = "1.005";
+our $VERSION = "1.007";
=head1 NAME
$link;
}
+sub save_columns {
+ my ($self, $table_object) = @_;
+
+ return $table_object->rowClass->columns;
+}
+
sub save_new {
my ($self, $req, $article, $articles) = @_;
my $cgi = $req->cgi;
my %data;
my $table_object = $self->table_object($articles);
- my @columns = $table_object->rowClass->columns;
+ my @columns = $self->save_columns($table_object);
$self->save_thumbnail($cgi, undef, \%data);
for my $name (@columns) {
$data{$name} = $cgi->param($name)
or $data{$col} = $self->default_value($req, \%data, $col);
}
- shift @columns;
- $article = $table_object->add(@data{@columns});
+ my @cols = $table_object->rowClass->columns;
+ shift @cols;
+ $article = $table_object->add(@data{@cols});
+
+ $self->save_new_more($req, $article, \%data);
# we now have an id - generate the links
return $article_data;
}
+sub save_more {
+ my ($self, $req, $article, $data) = @_;
+ # nothing to do here
+}
+
+sub save_new_more {
+ my ($self, $req, $article, $data) = @_;
+ # nothing to do here
+}
+
=item save
Error codes:
my $old_dynamic = $article->is_dynamic;
my $cgi = $req->cgi;
my %data;
- for my $name ($article->columns) {
+ my $table_object = $self->table_object($articles);
+ my @save_cols = $self->save_columns($table_object);
+ for my $name (@save_cols) {
$data{$name} = $cgi->param($name)
if defined($cgi->param($name)) and $name ne 'id' && $name ne 'parentid'
&& $req->user_can("edit_field_edit_$name", $article);
}
}
+ $self->save_more($req, $article, \%data);
+
if ($req->is_ajax) {
return $req->json_content
(
use BSE::CfgInfo 'product_options';
use BSE::Util::Tags qw(tag_hash);
-our $VERSION = "1.001";
+our $VERSION = "1.004";
=head1 NAME
return BSE::Arrows::make_arrows($req->cfg, $down_url, $up_url, $refresh, $args, id => $my_id, id_prefix => "prodoptmove");
}
+sub tag_tier_price {
+ my ($self, $rtier, $rprices, $product) = @_;
+
+ unless ($rprices->{loaded}) {
+ %$rprices = map { $_->tier_id => $_ } $product->prices
+ if $product->{id};
+ $rprices->{loaded} = 1;
+ }
+
+ $$rtier or return '** no current tier **';
+
+ exists $rprices->{$$rtier->id}
+ or return '';
+
+ return $rprices->{$$rtier->id}->retailPrice;
+}
+
+sub save_more {
+ my ($self, $req, $article, $data) = @_;
+
+ $self->_save_price_tiers($req, $article, $data);
+ $self->SUPER::save_more($req, $article, $data);
+}
+
+sub save_new_more {
+ my ($self, $req, $article, $data) = @_;
+
+ $self->_save_price_tiers($req, $article, $data);
+ $self->SUPER::save_new_more($req, $article, $data);
+}
+
+sub _save_price_tiers {
+ my ($self, $req, $article, $data) = @_;
+
+ $data->{save_pricing_tiers}
+ or return;
+
+ $req->user_can('edit_field_edit_retailPrice', $data)
+ or return;
+
+ my @tiers = Products->pricing_tiers;
+ my %prices;
+ for my $tier (@tiers) {
+ my $key = "tier_price_" . $tier->id;
+ if (exists $data->{$key} && $data->{$key} =~ /\S/) {
+ $prices{$tier->id} = $data->{$key} * 100;
+ }
+ }
+ $article->set_prices(\%prices);
+}
+
+sub save_columns {
+ my ($self, $table_object) = @_;
+
+ my @cols = $self->SUPER::save_columns($table_object);
+ my @tiers = Products->pricing_tiers;
+ if (@tiers) {
+ push @cols, "save_pricing_tiers";
+ push @cols, map { "tier_price_" . $_->id } @tiers;
+ }
+
+ return @cols;
+}
+
=head1 Edit tags
These a tags available on admin/edit_* pages specific to products.
dboptionsjson - returns the product options as JSON.
+=item *
+
+iterator begin price_tiers ... price_tier I<field> ... iterator end price_tiers
+
+Iterate over the configured price tiers.
+
+=item *
+
+tier_price
+
+Return the price at the current price_tier. Returns an empty string
+if there's no price at this tier.
+
=back
=cut
my $dboption_value_index;
my $current_option_value;
my $it = BSE::Util::Iterate->new;
+ my @tiers;
+ my $price_tier;
+ my %prices;
return
(
product => [ $tag_hash, $article ],
tag_dboptionvalue_move =>
$self, $req, $article, \@dboption_values, \$dboption_value_index
],
+ $it->make
+ (
+ single => "price_tier",
+ plural => "price_tiers",
+ code => [ pricing_tiers => "Products" ],
+ data => \@tiers,
+ store => \$price_tier,
+ ),
+ tier_price => [ tag_tier_price => $self, \$price_tier, \%prices, $article ],
);
}
}
}
+ if ($data->{save_pricing_tiers}) {
+ my @tiers = Products->pricing_tiers;
+ for my $tier (@tiers) {
+ my $key = "tier_price_" . $tier->id;
+ my $value = $data->{$key};
+ defined $value or next;
+ if ($value =~ /\S/ && $value !~ /^\d+(\.\d{1,2})?\s*/) {
+ $errors->{$key} = 'Pricing tier "' . $tier->description . '" price invalid';
+ }
+ }
+ }
+
return !keys %$errors;
}
--- /dev/null
+package BSE::TB::PriceTier;
+use strict;
+use base 'Squirrel::Row';
+
+our $VERSION = "1.000";
+
+sub columns {
+ return qw(id description group_id from_date to_date display_order);
+}
+
+sub table {
+ return "bse_price_tiers";
+}
+
+sub defaults {
+ return
+ (
+ group_id => undef,
+ from_date => undef,
+ to_date => undef,
+ display_order => time,
+ );
+}
+
+=item match($user, $date)
+
+Match the C<$user> and C<$date> against the contraints for this tier.
+
+Returns true if all constraints pass.
+
+=cut
+
+sub match {
+ my ($self, $user, $date) = @_;
+
+ if (my $from = $self->from_date) {
+ $date lt $from and return;
+ }
+
+ if (my $to = $self->to_date) {
+ $date gt $to and return;
+ }
+
+ if ($self->group_id) {
+ $user or return;
+
+ require BSE::TB::SiteUserGroups;
+ my $group = BSE::TB::SiteUserGroups->getById($self->group_id);
+ unless ($group) {
+ require BSE::TB::AuditLog;
+ BSE::TB::AuditLog->log
+ (
+ component => "shop:pricetier:match",
+ level => "crit",
+ actor => "S",
+ msg => "Unknown group id " . $self->group_id . " in price tier " . $self->id,
+ object => $self,
+ );
+ return;
+ }
+ $group->contains_user($user)
+ or return;
+ }
+
+ return 1;
+}
+
+1;
--- /dev/null
+package BSE::TB::PriceTierPrice;
+use strict;
+use base 'Squirrel::Row';
+
+our $VERSION = "1.000";
+
+sub table {
+ return "bse_price_tier_prices";
+}
+
+sub columns {
+ return qw(id tier_id product_id retailPrice);
+}
+
+1;
--- /dev/null
+package BSE::TB::PriceTierPrices;
+use strict;
+use base 'Squirrel::Table';
+use BSE::TB::PriceTierPrice;
+
+our $VERSION = "1.000";
+
+sub rowClass {
+ return "BSE::TB::PriceTierPrice";
+}
+
+1;
--- /dev/null
+package BSE::TB::PriceTiers;
+use strict;
+use base 'Squirrel::Table';
+use BSE::TB::PriceTier;
+
+our $VERSION = "1.000";
+
+sub rowClass { 'BSE::TB::PriceTier' }
+
+1;
use base 'Squirrel::Table';
use BSE::TB::SiteUserGroup;
-our $VERSION = "1.000";
+our $VERSION = "1.001";
use constant SECT_QUERY_GROUPS => "Query Groups";
use constant SECT_QUERY_GROUP_PREFIX => 'Query group ';
}
}
+sub getById {
+ my ($self, $id) = @_;
+
+ return $id > 0 ? $self->getByPkey($id) : $self->getQueryGroup(BSE::Cfg->single, $id);
+}
+
package BSE::TB::SiteUserQueryGroup;
use constant OWNER_TYPE => "G";
use Carp qw(confess);
use BSE::Countries qw(bse_country_code);
use BSE::Util::Secure qw(make_secret);
-our $VERSION = "1.009";
+our $VERSION = "1.010";
use constant MSG_SHOP_CART_FULL => 'Your shopping cart is full, please remove an item and try adding an item again';
{
productId => $product->{id},
units => $quantity,
- price=>$product->{retailPrice},
+ price=>$product->price(user => scalar $req->siteuser),
options=>$options,
%$extras,
};
{
productId => $addid,
units => $quantity,
- price=>$product->{retailPrice},
+ price=>$product->price(user => scalar $req->siteuser),
options=>$options,
%$extras,
};
{
productId => $product->{id},
units => $addition->{quantity},
- price=>$product->{retailPrice},
+ price=>$product->price(user => scalar $req->siteuser),
options=>[],
%{$addition->{extras}},
};
for my $col (@prodcols) {
$work{$col} = $product->$col() unless exists $work{$col};
}
- $work{extended_retailPrice} = $work{units} * $work{retailPrice};
+ $work{price} = $product->price(user => scalar $req->siteuser);
+ $work{extended_retailPrice} = $work{units} * $work{price};
$work{extended_gst} = $work{units} * $work{gst};
$work{extended_wholesale} = $work{units} * $work{wholesalePrice};
use base 'BSE::TagFormats';
use BSE::CfgInfo qw(custom_class);
-our $VERSION = "1.005";
+our $VERSION = "1.006";
sub new {
my ($class, $req) = @_;
dyncatmsg => [ tag_dyncatmsg => $self, $req ],
$self->dyn_iterator("userfiles", "userfile"),
$self->dyn_iterator_obj("paidfiles", "paidfile"),
+ price => [ tag_price => $self ],
+ ifTieredPricing => [ tag_ifTieredPricing => $self ],
$self->_custom_tags,
);
}
return make_arrows($self->{req}->cfg, $down_url, $up_url, $refresh_to, $img_prefix);
}
+=item price
+
+Return the price of a product.
+
+One of two parameters:
+
+=over
+
+=item *
+
+I<product> - the product to fetch the price for. This can be a name
+or [] evaluating to a product id.
+
+=item *
+
+I<field> - "price" to fetch the price, "discount" to fetch the
+difference from the base price, "discountpc" to fetch the discount in
+percent (whole number). Returns the price if no I<field> is
+specified.
+
+=back
+
+=cut
+
+sub tag_price {
+ my ($self, $args, $acts, $func, $templater) = @_;
+
+ my ($id, $field) = $templater->get_parms($args, $acts);
+ $field ||= "price";
+
+ my $work;
+ if ($id =~ /^[0-9]+$/) {
+ require Products;
+ $work = Products->getByPkey($id)
+ or return "** unknown product $id **";
+ }
+ else {
+ $work = $self->{req}->get_article($id)
+ or return "** unknown product name $id **";
+ }
+
+ my ($price, $tier) = $work->price(user => scalar $self->{req}->siteuser);
+
+ if ($field eq "price") {
+ return $price;
+ }
+ elsif ($field eq "discount") {
+ return $work->retailPrice - $price;
+ }
+ elsif ($field eq "discountpc") {
+ $work->retailPrice or return "";
+ return sprintf("%.0f", ($work->retailPrice - $price) / $work->retailPrice * 100);
+ }
+ else {
+ return "** unknown field $field **";
+ }
+}
+
+=item ifTieredPricing
+
+Conditional to check if there's tiered pricing.
+
+=cut
+
+sub tag_ifTieredPricing {
+ require Products;
+ my @tiers = Products->pricing_tiers;
+
+ return scalar @tiers;
+}
+
1;
=head1 NAME
use vars qw/@ISA/;
@ISA = qw/Article/;
-our $VERSION = "1.000";
+our $VERSION = "1.001";
# subscription_usage values
use constant SUBUSAGE_START_ONLY => 1;
# remove any wishlist items
BSE::DB->run(bseRemoveProductFromWishlists => $self->id);
+ # remove any tiered prices
+ BSE::DB->run(bseRemoveProductPrices => $self->id);
+
return $self->SUPER::remove($cfg);
}
return $row->{have_sale_files};
}
+sub prices {
+ my ($self) = @_;
+
+ require BSE::TB::PriceTierPrices;
+ my @prices = BSE::TB::PriceTierPrices->getBy(product_id => $self->id);
+}
+
+=item set_prices($prices)
+
+Set tiered pricing for the product.
+
+I<$prices> is a hashref mapping tier ids to prices in cents.
+
+If a tier doesn't have a price in I<$prices> it's removed from the
+product.
+
+=cut
+
+sub set_prices {
+ my ($self, $prices) = @_;
+
+ my %current = map { $_->tier_id => $_ } $self->prices;
+ for my $tier_id (keys %$prices) {
+ my $current = delete $current{$tier_id};
+ if ($current) {
+ $current->set_retailPrice($prices->{$tier_id});
+ $current->save;
+ }
+ else {
+ BSE::TB::PriceTierPrices->make
+ (
+ tier_id => $tier_id,
+ product_id => $self->id,
+ retailPrice => $prices->{$tier_id},
+ );
+ }
+ }
+
+ # remove any spares
+ for my $price (values %current) {
+ $price->remove;
+ }
+}
+
+=item price(user => $user, date => $sql_date)
+
+=item price(user => $user)
+
+Return the retail price depending on the user and date
+and optionally the tier object (in list context).
+
+If no tier matches then the undef is returned at the tier object.
+
+=cut
+
+sub price {
+ my ($self, %opts) = @_;
+
+ my $user = delete $opts{user};
+ my $date = delete $opts{date} || BSE::Util::SQL::now_sqldate();
+ my @tiers = Products->pricing_tiers;
+ my %prices = map { $_->tier_id => $_ } $self->prices;
+
+ my $price;
+ my $found_tier;
+ for my $tier (@tiers) {
+ if ($prices{$tier->id}
+ && $tier->match($user, $date)) {
+ $price = $prices{$tier->id}->retailPrice;
+ $found_tier = $tier;
+ last;
+ }
+ }
+
+ defined $price or $price = $self->retailPrice;
+
+ return wantarray ? ( $price, $found_tier ) : $price;
+}
+
+sub update_dynamic {
+ my ($self, $cfg) = @_;
+
+ my @tiers = Products->pricing_tiers;
+ if (@tiers) {
+ $self->set_cached_dynamic(1);
+ return;
+ }
+
+ return $self->SUPER::update_dynamic($cfg);
+}
+
package BSE::CfgProductOption;
use strict;
@ISA = qw(Squirrel::Table);
use Product;
-our $VERSION = "1.000";
+our $VERSION = "1.001";
sub rowClass {
return 'Product';
return Products->getSpecial(visibleStep => $id, $today);
}
+{
+ my $tiers;
+ sub pricing_tiers {
+ unless ($tiers) {
+ require BSE::TB::PriceTiers;
+ $tiers = [ sort { $a->display_order <=> $b->display_order }
+ BSE::TB::PriceTiers->all ];
+ }
+
+ return @$tiers;
+ }
+}
+
1;
--
-# VERSION=1.001
+# VERSION=1.002
name: bse_siteuserSeminarBookingsDetail
sql_statement: <<SQL
select ar.*, pr.*, se.*, ss.*, sb.*,
and owner_id = ?
SQL
+name: bseRemoveProductPrices
+sql_statement: <<SQL
+delete from bse_price_tier_prices
+where product_id = ?
+SQL
-<:wrap admin/xbase.tmpl title => "Shop Administration", menuitem=>edit, showtitle=>1 :>
+<:wrap admin/xbase.tmpl title => "Shop Administration", menuitem=>"edit", showtitle=>"1" :>
<:ifMessage:>
<p><b><:message:></b></p>
<:or:><:eif:>
(0.00)<:or:><:money product retailPrice:><:eif:> </td>
<td class="help"><:help product retail:> <:error_img retailPrice:></td>
</tr>
+<:if Price_tiers:>
+ <tr>
+ <th><:cfg "product field" tier_prices "Tier prices":>:</th>
+ <td>
+<input type="hidden" name="save_pricing_tiers" value="1" />
+ <table class="editform editformtiny">
+<:iterator begin price_tiers:>
+<tr>
+ <th><:price_tier description:></th>
+ <td>$<:if FieldPerm retailPrice:><input type="text" name="tier_price_<:price_tier id:>" value="<:ifEq [tier_price] "":><:oldi [cat "tier_price_" [price_tier id]] 0:><:or:><:oldi [cat "tier_price_" [price_tier id]] 0 money tier_price:><:eif:>" size="7" /> (0.00)<:or FieldPerm:><:ifEq [tier_price] "":>-<:or:><:money tier_price:><:eif:><:eif FieldPerm:></td>
+ <td class="help"><:error_img [cat "tier_price_" [price_tier id]]:></td>
+
+</tr>
+<:iterator end price_tiers:>
+ </table>
+ </td>
+<td class="help"><:help product tier_price:></td>
+ </tr>
+<:or Price_tiers:><:eif Price_tiers:>
<tr>
<th>Wholesale price:</th>
<td>$
<td nowrap align="center">
<input type="text" name="quantity_<:index:>" size="2" value="<:item units:>">
</td>
- <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:money
- item retailPrice:></b></font></td>
+ <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:money item price:></b></font></td>
<td nowrap>
<input type="submit" name="delete_<:index:>" value="Remove">
</td>
<td><font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#FFFFFF"><b>Qty:</b></font></td>
<:or:><:eif:>
</tr>
+<:if Dynamic:>
+ <:iterator begin dynallprods:>
+ <tr valign="middle" align="center" bgcolor="#FFFFFF">
+ <td width="100%" align="left"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:url dynallprod:>"><:dynallprod
+ description:></a></font> <:dynmoveallprod:></td>
+<:if Dynallprod retailPrice:>
+ <td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:money price dynallprod:></b></font></td>
+<:ifAnd [cfg shop order_from_catalog] [ifEq [ifDynAnyProductOptions dynallprod] "0"]:>
+<td nowrap="nowrap"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><input type="text" name="qty<:dynallprod id:>" size="4" /><input type="image" src="/images/store/quick_add.gif" alt="+" name="a_addsingle<:dynallprod id:>" /></font></td>
+<:or:><:eif:>
+<:or Dynallprod:>
+ <td align="center"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2" color="#999999"><b>TBA</b></font></td>
+<:eif Dynallprod:>
+ </tr>
+
+ <:iterator end dynallprods:>
+<:or Dynamic:>
<:iterator begin allprods:>
<tr valign="middle" align="center" bgcolor="#FFFFFF">
<td width="100%" align="left"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><a href="<:url allprod:>"><:allprod
</tr>
<:iterator end allprods:>
+<:eif Dynamic:>
<:ifCfg shop order_from_catalog:>
<tr>
<td colspan="3" align="right" bgcolor="#FFFFFF"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><input type="submit" name="a_addmultiple" value="Add" /></font></td>
<td nowrap align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item
units:></font></td>
<td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:
- money item retailPrice :></b></font></td>
+ money item price :></b></font></td>
</tr>
<:iterator end items:>
<:if Shipping_cost:>
<td nowrap align="center"><font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><:item
units:></font></td>
<td align="right"> <font face="Verdana, Arial, Helvetica, sans-serif" size="-2"><b>$<:
- money item retailPrice :></b></font></td>
+ money item price :></b></font></td>
</tr>
<:iterator end items:>
<:if shipping_cost:>
<td bgcolor="#666666" align="left"> <font size="2" face="Verdana, Arial, Helvetica, sans-serif" color="#FFFFFF">
<b>Price:</b></font> </td>
<td bgcolor="#FFFFFF"> <font face="Verdana, Arial, Helvetica, sans-serif" size="2" color="#000000">
- <b>$<:money product retailPrice:> </b>(inc GST)</font> </td>
+ <b>$<:if Dynamic:><:money price dynarticle:> <:ifPrice dynarticle discountpc:><:price dynarticle discountpc:>% off!<:or:><:eif:><:or Dynamic:><:money product retailPrice:><:eif Dynamic:></b>(inc GST)</font> </td>
</tr>
</table>
</td>
Column filekey;varchar(80);NO;;
Index PRIMARY;1;[id]
Index by_owner_category;0;[owner_type;owner_id;category]
+Table bse_price_tier_prices
+Engine MyISAM
+Column id;int(11);NO;NULL;auto_increment
+Column tier_id;int(11);NO;NULL;
+Column product_id;int(11);NO;NULL;
+Column retailPrice;int(11);NO;NULL;
+Index PRIMARY;1;[id]
+Index tier_product;1;[tier_id;product_id]
+Table bse_price_tiers
+Engine MyISAM
+Column id;int(11);NO;NULL;auto_increment
+Column description;text;NO;NULL;
+Column group_id;int(11);YES;NULL;
+Column from_date;date;YES;NULL;
+Column to_date;date;YES;NULL;
+Column display_order;int(11);YES;NULL;
+Index PRIMARY;1;[id]
Table bse_product_option_values
Engine InnoDB
Column id;int(11);NO;NULL;auto_increment