Adding pluggable tax system.

This commit is contained in:
Martin Kamerbeek 2009-04-17 13:32:18 +00:00
parent eec6ac3e47
commit 2e4ce42b76
9 changed files with 366 additions and 590 deletions

View file

@ -25,6 +25,8 @@
- fixed #10190: Matrix 2.0 - Strange characters in "Show Ratings" box
- rfe: #10002: User Manager, view User's Profile (perlDreamer Consulting)
- rfe: #10073: Account system to be able to return XML/JSON (perlDreamer Consulting)
- Replaced the tax system with a pluggable one. Included are a Generic plugin
(which works the same as the old system) and a plugin for EU merchants ( Martin Kamerbeek / Oqapi )
7.7.3
- fixed #10094: double explanation in thread help

View file

@ -36,6 +36,9 @@ extendSchedulerFields($session);
allMaintenanceSingleton($session);
unsetPackageFlags($session);
installPluggableTax( $session );
finish($session); # this line required
#----------------------------------------------------------------------------
@ -149,6 +152,60 @@ sub _getAnswer{
return $answer;
}
#----------------------------------------------------------------------------
sub installPluggableTax {
my $session = shift;
my $db = $session->db;
print "\tInstall tables for pluggable tax system..." unless $quiet;
# Rename table for the Generic tax plugin
$db->write( 'alter table tax rename tax_generic_rates' );
# Create tax driver table
$db->write( 'create table taxDriver (className char(255) not null primary key, options mediumtext)' );
# Table for storing EU VAT numbers.
$db->write( <<EOSQL2 );
create table tax_eu_vatNumbers (
userId char(22) binary not null,
countryCode char(3) not null,
vatNumber char(20) not null,
approved tinyint(1) not null default 0,
primary key( userId, vatNumber )
);
EOSQL2
# Add the Generic and EU taxdrivers to the config file.
$session->config->set( 'taxDrivers', [
'WebGUI::Shop::TaxDriver::Generic',
'WebGUI::Shop::TaxDriver::EU',
] );
# Add a setting to store the active tax plugin.
$session->setting->add( 'activeTaxPlugin', 'WebGUI::Shop::TaxDriver::Generic' );
# Add column to sku for storing each sku's tax configuration.
$db->write( "alter table sku add column taxConfiguration mediumtext " );
# Migrate the tax overrides of skus into the tax configuration column.
# Don't use getLineage because this has to be done for each revision.
my $sth = $db->read( "select assetId, revisionDate, overrideTaxRate, taxRateOverride from sku" );
while (my $row = $sth->hashRef) {
my $config = {
overrideTaxRate => $row->{ overrideTaxRate } || 0,
taxRateOverride => $row->{ taxRateOverride } || 0,
};
$db->write( 'update sku set taxConfiguration=? where assetId=? and revisionDate=?', [
to_json( $config ),
$row->{ assetId },
$row->{ revisionDate },
]);
}
$sth->finish;
print "Done.\n" unless $quiet;
}

View file

@ -53,6 +53,9 @@ sub appendCommonVars {
$var->{ 'view_sales_url' } = $self->getUrl( 'module=shop;do=viewSales' );
$var->{ 'viewSalesIsActive' } = $method eq 'viewSales';
$var->{ 'manage_tax_url' } = $self->getUrl( 'module=shop;do=manageTaxData' );
$var->{ 'manageTaxIsActive' } = $method eq 'manageTaxData';
}
#-------------------------------------------------------------------
@ -201,6 +204,17 @@ sub www_managePurchases {
return $self->processTemplate($var,$session->setting->get("shopMyPurchasesTemplateId"));
}
#-------------------------------------------------------------------
sub www_manageTaxData {
my $self = shift;
my $session = $self->session;
my $userScreen = WebGUI::Shop::Tax->new( $session )->getDriver->getUserScreen;
return $userScreen;
}
#-------------------------------------------------------------------
=head2 www_view ( )

View file

@ -20,7 +20,7 @@ use base 'WebGUI::Asset';
use WebGUI::International;
use WebGUI::Inbox;
use WebGUI::Shop::Cart;
use JSON qw{ from_json to_json };
=head1 NAME
@ -102,6 +102,7 @@ sub definition {
my $definition = shift;
my %properties;
tie %properties, 'Tie::IxHash';
my $i18n = WebGUI::International->new($session, "Asset_Sku");
%properties = (
description => {
@ -125,20 +126,6 @@ sub definition {
label => $i18n->get("display title"),
hoverHelp => $i18n->get("display title help")
},
overrideTaxRate => {
tab => "shop",
fieldType => "yesNo",
defaultValue => 0,
label => $i18n->get("override tax rate"),
hoverHelp => $i18n->get("override tax rate help")
},
taxRateOverride => {
tab => "shop",
fieldType => "float",
defaultValue => 0.00,
label => $i18n->get("tax rate override"),
hoverHelp => $i18n->get("tax rate override help")
},
vendorId => {
tab => "shop",
fieldType => "vendor",
@ -146,6 +133,11 @@ sub definition {
label => $i18n->get("vendor"),
hoverHelp => $i18n->get("vendor help")
},
taxConfiguration => {
noFormPost => 1,
fieldType => 'hidden',
defaultValue => '{}',
},
);
push(@{$definition}, {
assetName=>$i18n->get('assetName'),
@ -206,6 +198,31 @@ sub getConfiguredTitle {
return $self->getTitle;
}
#-------------------------------------------------------------------
sub getEditForm {
my $self = shift;
my $session = $self->session;
my $tabform = $self->SUPER::getEditForm;
# Let the tax system add the form fields that are required by the active tax plugin for configuring the sku tax.
# WebGUI::Shop::Tax->new( $session )->appendSkuForm( $self->getId, $tabform->getTab('shop') );
my $taxDriver = WebGUI::Shop::Tax->getDriver( $session );
my $definition = $taxDriver->skuFormDefinition;
my $config = $self->getTaxConfiguration( $taxDriver->className );
my $shop = $tabform->getTab( 'shop' );
foreach my $fieldName ( keys %{ $definition } ) {
$shop->dynamicField(
%{ $definition->{ $fieldName } },
name => $fieldName,
value => $config->{ $fieldName },
);
}
return $tabform;
}
#-------------------------------------------------------------------
@ -287,6 +304,20 @@ sub getRecurInterval {
return undef;
}
#-------------------------------------------------------------------
sub getTaxConfiguration {
my $self = shift;
my $namespace = shift;
my $configs = eval { from_json( $self->getValue('taxConfiguration') ) };
if ($@) {
$self->session->log->error( 'Tax configuration of asset ' . $self->getId . ' appears to be corrupt. :' . $@ );
return undef;
}
return $configs->{ $namespace }
}
#-------------------------------------------------------------------
=head2 getTaxRate ( )
@ -528,6 +559,20 @@ sub onRemoveFromCart {
return undef;
}
#-------------------------------------------------------------------
sub processPropertiesFromFormPost {
my $self = shift;
my $output = $self->SUPER::processPropertiesFromFormPost( @_ );
my $taxDriver = WebGUI::Shop::Tax->new( $self->session )->getDriver;
$self->session->log->fatal( 'Could not instanciate tax driver.' ) unless $taxDriver;
$self->setTaxConfiguration( $taxDriver->className, $taxDriver->processSkuFormPost );
return $output;
}
#-------------------------------------------------------------------
=head2 processStyle ( output )
@ -546,6 +591,28 @@ sub processStyle {
return $self->getParent->processStyle($output);
}
#-------------------------------------------------------------------
sub setTaxConfiguration {
my $self = shift;
my $namespace = shift;
my $configuration = shift;
# Fetch current tax configurations
my $configs = eval { from_json( $self->getValue('taxConfiguration') ) };
if ($@) {
$self->session->log->error( 'Tax configuration of asset ' . $self->getId . ' is corrupt.' );
return undef;
}
# Apply the new configuration for the given driver...
$configs->{ $namespace } = $configuration;
# ...and persist it to the db.
$self->update( {
taxConfiguration => to_json( $configs ),
} );
}
#-------------------------------------------------------------------
=head2 www_view ( )

View file

@ -820,7 +820,7 @@ sub www_view {
$self->update({shippingAddressId=>''});
}
# if there is no shipping address we can't check out
if (WebGUI::Error->caught) {
$var{shippingPrice} = $var{tax} = $self->formatCurrency(0);
@ -830,7 +830,6 @@ sub www_view {
else {
$var{hasShippingAddress} = 1;
$var{shippingAddress} = $address->getHtmlFormatted;
$var{tax} = $self->calculateTaxes;
my $ship = WebGUI::Shop::Ship->new($self->session);
my $options = $ship->getOptions($self);
my %formOptions = ();
@ -843,7 +842,10 @@ sub www_view {
$var{shippingPrice} = ($self->get("shipperId") ne "") ? $options->{$self->get("shipperId")}{price} : $options->{$defaultOption}{price};
$var{shippingPrice} = $self->formatCurrency($var{shippingPrice});
}
# Tax variables
$var{tax} = $self->calculateTaxes;
# POS variables
$var{isCashier} = WebGUI::Shop::Admin->new($session)->isCashier;
$var{posLookupForm} = WebGUI::Form::email($session, {name=>"posEmail"})

View file

@ -3,12 +3,9 @@ package WebGUI::Shop::Tax;
use strict;
use Class::InsideOut qw{ :std };
use WebGUI::Text;
use WebGUI::Storage;
use WebGUI::Exception::Shop;
use WebGUI::Shop::Admin;
use WebGUI::Shop::Cart;
use WebGUI::Shop::CartItem;
use WebGUI::Pluggable;
use List::Util qw{sum};
=head1 NAME
@ -21,10 +18,6 @@ This package manages tax information, and calculates taxes on a shopping cart.
in that the only data it contains is a WebGUI::Session object, but it does provide several methods for
handling the information in the tax tables.
Taxes are accumulated through increasingly specific geographic information. For example, you can
specify the sales tax for a whole country, then the additional sales tax for a state in the country,
all the way down to a single code inside of a city.
=head1 SYNOPSIS
use WebGUI::Shop::Tax;
@ -39,65 +32,36 @@ These subroutines are available from this package:
readonly session => my %session;
#-------------------------------------------------------------------
=head2 add ( [$params] )
Add tax information to the table. Returns the taxId of the newly created tax information.
=head3 $params
A hash ref of the geographic and rate information. The country and taxRate parameters
must have defined values.
=head4 country
The country this tax information applies to.
=head4 state
The state this tax information applies to. state and country together are unique.
=head4 city
The ciy this tax information applies to. Cities are unique with state and country information.
=head4 code
The postal code this tax information applies to. codes are unique with state and country information.
=head4 taxRate
This is the tax rate for the location, as specified by the geographical
fields country, state, city and/or code. The tax rate is stored as
a percentage, like 5.5 .
=cut
sub add {
my $self = shift;
my $params = shift;
WebGUI::Error::InvalidParam->throw(error => 'Must pass in a hashref of params')
unless ref($params) eq 'HASH';
WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'country')
unless exists($params->{country}) and $params->{country};
WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'taxRate')
unless exists($params->{taxRate}) and defined $params->{taxRate};
$params->{taxId} = 'new';
my $id = $self->session->db->setRow('tax', 'taxId', $params);
return $id;
}
##-------------------------------------------------------------------
#sub appendSkuForm {
# my $self = shift;
# my $assetId = shift;
# my $form = shift;
# my $db = $self->session->db;
#
# my $values = $db->buildHashRef( 'select name, value from skuTaxConfiguration where assetId=?', [
# $assetId,
# ] );
#
# my $definition = $self->getDriver->skuFormDefinition;
# foreach my $fieldName (keys %{ $definition }) {
# $form->dynamicField(
# %{ $definition->{ $fieldName } },
# name => $fieldName,
# value => $values->{ $fieldName },
# );
# }
#}
#-------------------------------------------------------------------
=head2 calculate ( $cart )
Calculate the tax for the contents of the cart. The tax rate is calculated off
of the shipping address stored in the cart. If an item in the cart has an alternate
address, that is used instead. Finally, if the item in the cart has a Sku with a tax
rate override, that rate overrides all. Returns 0 if no shipping address has been attached to the cart yet.
Calculate the tax for the contents of the cart.
=head3 cart
An instanciated cart object.
=cut
@ -107,241 +71,60 @@ sub calculate {
WebGUI::Error::InvalidParam->throw(error => 'Must pass in a WebGUI::Shop::Cart object')
unless ref($cart) eq 'WebGUI::Shop::Cart';
my $book = $cart->getAddressBook;
return 0 if $cart->get('shippingAddressId') eq "";
my $address = $book->getAddress($cart->get('shippingAddressId'));
my $tax = 0;
##Fetch the tax data for the cart address so it doesn't have to look it up for every item
##in the cart with that address.
my $cartTaxables = $self->getTaxRates($address);
# Fetch the default shipping address for each item in the cart that hasn't set its own.
my $shippingAddress = $book->getAddress( $cart->get('shippingAddressId') ) if $cart->get('shippingAddressId');
my $driver = $self->getDriver;
my $tax = 0;
foreach my $item (@{ $cart->getItems }) {
my $sku = $item->getSku;
my $unitPrice = $sku->getPrice;
my $quantity = $item->get('quantity');
##Check for an item specific shipping address
my $taxables;
my $sku = $item->getSku;
my $quantity = $item->get('quantity');
my $unitPrice = $sku->getPrice;
# Check if this cart item overrides the shipping address. If it doesn't, use the default shipping address.
my $itemAddress = $shippingAddress;
if (defined $item->get('shippingAddressId')) {
my $itemAddress = $book->getAddress($item->get('shippingAddressId'));
$taxables = $self->getTaxRates($itemAddress);
$itemAddress = $book->getAddress($item->get('shippingAddressId'));
}
else {
$taxables = $cartTaxables;
}
##Check for a SKU specific tax override rate
my $skuTaxRate = $sku->getTaxRate();
my $itemTax;
if (defined $skuTaxRate) {
$itemTax = $skuTaxRate;
}
else {
$itemTax = sum(@{$taxables});
}
$itemTax /= 100;
$tax += $unitPrice * $quantity * $itemTax;
my $taxRate = $driver->getTaxRate( $sku, $itemAddress );
# Calc the monetary tax for the given quantity of this item and add it to the total.
$tax += $unitPrice * $quantity * $taxRate / 100;
}
return $tax;
}
#-------------------------------------------------------------------
=head2 delete ( [$params] )
=head2 getDriver ( [ $session ] )
Deletes data from the tax table by taxId.
Return an instance of the enabled tax driver. This method can be invoked both as class or instance method. If you
invoke this method as a class method you must pass a WebGUI::Session object.
=head3 $params
=head3 session
A hashref containing the taxId of the data to delete from the table.
=head4 taxId
The taxId of the data to delete from the table.
A WebGUI::Session object. Required in class context, optional in instance context.
=cut
sub delete {
my $self = shift;
my $params = shift;
WebGUI::Error::InvalidParam->throw(error => 'Must pass in a hashref of params')
unless ref($params) eq 'HASH';
WebGUI::Error::InvalidParam->throw(error => "Hash ref must contain a taxId key with a defined value")
unless exists($params->{taxId}) and defined $params->{taxId};
$self->session->db->write('delete from tax where taxId=?', [$params->{taxId}]);
return;
}
#-------------------------------------------------------------------
=head2 exportTaxData ( )
Creates a tab deliniated file containing all the information from
the tax table. Returns a temporary WebGUI::Storage object containing
the file. The file will be named "siteTaxData.csv".
=cut
sub exportTaxData {
my $self = shift;
my $taxIterator = $self->getItems;
my @columns = grep { $_ ne 'taxId' } $taxIterator->getColumnNames;
my $taxData = WebGUI::Text::joinCSV(@columns) . "\n";
while (my $taxRow = $taxIterator->hashRef() ) {
my @taxData = @{ $taxRow }{@columns};
foreach my $column (@taxData) {
$column =~ tr/,/|/; ##Convert to the alternation syntax for the text file
}
$taxData .= WebGUI::Text::joinCSV(@taxData) . "\n";
sub getDriver {
my $self = shift;
my $session = shift || $self->session;
my $className = $session->setting->get( 'activeTaxPlugin' );
my $driver = eval {
WebGUI::Pluggable::instanciate( $className, 'new', [ $session ] );
};
if ($@) {
$session->log->error("Can't instanciate tax driver [$className] because $@");
return undef;
}
my $storage = WebGUI::Storage->createTemp($self->session);
$storage->addFileFromScalar('siteTaxData.csv', $taxData);
return $storage;
}
#-------------------------------------------------------------------
=head2 getAllItems ( )
Returns an arrayref of hashrefs, where each hashref is the data for one row of
tax data. taxId is dropped from the dataset.
=cut
sub getAllItems {
my $self = shift;
my $taxes = $self->session->db->buildArrayRefOfHashRefs('select country,state,city,code,taxRate from tax order by country, state');
return $taxes;
}
#-------------------------------------------------------------------
=head2 getItems ( )
Returns a WebGUI::SQL::Result object for accessing all of the data in the tax table. This
is a convenience method for listing and/or exporting tax data.
=cut
sub getItems {
my $self = shift;
my $result = $self->session->db->read('select * from tax order by country, state');
return $result;
}
#-------------------------------------------------------------------
=head2 getTaxRates ( $address )
Given a WebGUI::Shop::Address object, return all rates associated with the address as an arrayRef.
=cut
sub getTaxRates {
my $self = shift;
my $address = shift;
WebGUI::Error::InvalidObject->throw(error => 'Need an address.', expected=>'WebGUI::Shop::Address', got=>(ref $address))
unless ref($address) eq 'WebGUI::Shop::Address';
my $country = $address->get('country');
my $state = $address->get('state');
my $city = $address->get('city');
my $code = $address->get('code');
my $result = $self->session->db->buildArrayRef(
q{
select taxRate from tax where find_in_set(?, country)
and (state='' or find_in_set(?, state))
and (city='' or find_in_set(?, city))
and (code='' or find_in_set(?, code))
},
[ $country, $state, $city, $code, ]);
return $result;
}
#-------------------------------------------------------------------
=head2 importTaxData ( $filePath )
Import tax information from the specified file in CSV format. The
first line of the file should contain only the name of the columns, in
any order. It may not contain any comments.
These are the column names, each is required:
=over 4
=item *
country
=item *
state
=item *
city
=item *
code
=item *
taxRate
=back
The following lines will contain tax information. Blank
lines and anything following a '#' sign will be ignored from
the second line of the file, on to the end.
Returns 1 if the import has taken place. This is to help you know
if old data has been deleted and new has been inserted. If an error is
detected, it will throw exceptions.
=head3 $filePath
The path to a file with data to import into the Product system.
=cut
sub importTaxData {
my $self = shift;
my $filePath = shift;
WebGUI::Error::InvalidParam->throw(error => q{Must provide the path to a file})
unless $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File could not be found}, brokenFile => $filePath)
unless -e $filePath;
WebGUI::Error::InvalidFile->throw(error => qq{File is not readable}, brokenFile => $filePath)
unless -r $filePath;
open my $table, '<', $filePath or
WebGUI::Error->throw(error => qq{Unable to open $filePath for reading: $!\n});
my $headers;
$headers = <$table>;
chomp $headers;
my @headers = WebGUI::Text::splitCSV($headers);
WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath)
unless (join(q{-}, sort @headers) eq 'city-code-country-state-taxRate')
and (scalar @headers == 5);
my @taxData = ();
my $line = 1;
while (my $taxRow = <$table>) {
chomp $taxRow;
$taxRow =~ s/\s*#.+$//;
next unless $taxRow;
local $_;
my @taxRow = map { tr/|/,/; $_; } WebGUI::Text::splitCSV($taxRow);
WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line)
unless scalar @taxRow == 5;
push @taxData, [ @taxRow ];
}
##Okay, if we got this far, then the data looks fine.
return unless scalar @taxData;
$self->session->db->beginTransaction;
$self->session->db->write('delete from tax');
foreach my $taxRow (@taxData) {
my %taxRow;
@taxRow{ @headers } = @{ $taxRow }; ##Must correspond 1:1, or else...
$self->add(\%taxRow);
}
$self->session->db->commit;
return 1;
return $driver;
}
#-------------------------------------------------------------------
@ -362,6 +145,8 @@ sub new {
return $self;
}
#-------------------------------------------------------------------
=head2 session ( )
@ -372,148 +157,35 @@ Accessor for the session object. Returns the session object.
#-------------------------------------------------------------------
=head2 www_deleteTax ( )
=head2 www_do ( )
Delete a row of tax information, using the form variable taxId as
the id of the row to delete.
Allows tax drivers to define their own www_ methods. Pass the www_ method that must be executed in the 'do' form
var.
=cut
sub www_deleteTax {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $taxId = $session->form->get('taxId');
$self->delete({ taxId => $taxId });
return $self->www_manage;
}
#-------------------------------------------------------------------
=head2 www_addTax ( )
Add new tax information into the database, via the UI.
=cut
sub www_addTax {
sub www_do {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $params;
my ($form) = $session->quick('form');
$params->{country} = $form->get('country', 'text');
$params->{state} = $form->get('state', 'text');
$params->{city} = $form->get('city', 'text');
$params->{code} = $form->get('code', 'text');
$params->{taxRate} = $form->get('taxRate', 'float');
$self->add($params);
return $self->www_manage;
}
#-------------------------------------------------------------------
my $taxDriver = $self->getDriver;
my $method = 'www_' . $session->form->process( 'do' );
=head2 www_exportTax ( )
return "Invalid method name" unless $method =~ m{ ^[a-zA-Z0-9_]+$ }xms;
Export the entire tax table as a CSV file the user can download.
if ( $taxDriver->can( $method ) ) {
my $output = eval{ $taxDriver->$method };
=cut
sub www_exportTax {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $storage = $self->exportTaxData();
$self->session->http->setRedirect($storage->getUrl($storage->getFiles->[0]));
return "redirect";
}
#-------------------------------------------------------------------
=head2 www_getTaxesAsJson ( )
Servers side pagination for tax data that is sent as JSON back to the browser to be
displayed in a YUI DataTable.
=cut
sub www_getTaxesAsJson {
my ($self) = @_;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my ($db, $form) = $session->quick(qw(db form));
my $startIndex = $form->get('startIndex') || 0;
my $numberOfResults = $form->get('results') || 25;
my %goodKeys = qw/country 1 state 1 city 1 code 1 'tax rate' 1/;
my $sortKey = $form->get('sortKey');
$sortKey = $goodKeys{$sortKey} == 1 ? $sortKey : 'country';
my $sortDir = $form->get('sortDir');
$sortDir = lc($sortDir) eq 'desc' ? 'desc' : 'asc';
my @placeholders = ();
my $sql = 'select SQL_CALC_FOUND_ROWS * from tax';
my $keywords = $form->get("keywords");
if ($keywords ne "") {
$db->buildSearchQuery(\$sql, \@placeholders, $keywords, [qw{country state city code}])
}
push(@placeholders, $startIndex, $numberOfResults);
$sql .= sprintf (" order by %s limit ?,?","$sortKey $sortDir");
my %results = ();
my @records = ();
my $sth = $db->read($sql, \@placeholders);
while (my $record = $sth->hashRef) {
push(@records,$record);
}
$results{'recordsReturned'} = $sth->rows()+0;
$sth->finish;
$results{'records'} = \@records;
$results{'totalRecords'} = $db->quickScalar('select found_rows()')+0; ##Convert to numeric
$results{'startIndex'} = $startIndex;
$results{'sort'} = undef;
$results{'dir'} = $sortDir;
$session->http->setMimeType('application/json');
return JSON::to_json(\%results);
}
#-------------------------------------------------------------------
=head2 www_importTax ( )
Import new tax data from a file provided by the user. This will replace the current
data with the new data.
=cut
sub www_importTax {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
my $storage = WebGUI::Storage->create($session);
my $taxFile = $storage->addFileFromFormPost('importFile', 1);
eval {
$self->importTaxData($storage->getPath($taxFile)) if $taxFile;
};
my ($exception, $status_message);
if ($exception = Exception::Class->caught('WebGUI::Error::InvalidFile')) {
$status_message = sprintf 'A problem was found with your file: %s',
$exception->error;
if ($exception->brokenLine) {
$status_message .= sprintf ' on line %d', $exception->brokenLine;
if ($@) {
$session->log->error("An error occurred while executing method [$method] on active tax driver: $@");
return "An error occurred while executing a method on a tax driver. Please consult the webgui log.";
}
else {
return $output || $self->www_manage;
}
}
elsif ($exception = Exception::Class->caught()) {
$status_message = sprintf 'A problem happened during the import: %s', $exception->error;
}
return $self->www_manage($status_message);
return "Cannot call method [$method] on active tax driver.";
}
#-------------------------------------------------------------------
@ -531,154 +203,107 @@ import.
=cut
sub www_manage {
my $self = shift;
my $status_message = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new($session);
return $session->privilege->insufficient
unless $admin->canManage;
##YUI specific datatable CSS
my ($style, $url) = $session->quick(qw(style url));
$style->setLink($url->extras('/yui/build/fonts/fonts-min.css'), {rel=>'stylesheet', type=>'text/css'});
$style->setLink($url->extras('yui/build/datatable/assets/skins/sam/datatable.css'), {rel=>'stylesheet', type => 'text/CSS'});
$style->setLink($url->extras('yui/build/paginator/assets/skins/sam/paginator.css'), {rel=>'stylesheet', type => 'text/CSS'});
$style->setScript($url->extras('/yui/build/utilities/utilities.js'), {type=>'text/javascript'});
$style->setScript($url->extras('yui/build/json/json-min.js'), {type => 'text/javascript'});
$style->setScript($url->extras('yui/build/paginator/paginator-min.js'), {type => 'text/javascript'});
$style->setScript($url->extras('yui/build/datasource/datasource-min.js'), {type => 'text/javascript'});
##YUI Datatable
$style->setScript($url->extras('yui/build/datatable/datatable-min.js'), {type => 'text/javascript'});
##Default CSS
$style->setRawHeadTags('<style type="text/css"> #paging a { color: #0000de; } #search, #export form { display: inline; } </style>');
my $i18n=WebGUI::International->new($session, 'Tax');
my $self = shift;
my $status_message = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new( $session );
my $exportForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=tax;method=exportTax')})
. WebGUI::Form::submit($session,{value=>$i18n->get('export tax','Shop'), extras=>q{style="float: left;"} })
. WebGUI::Form::formFooter($session);
my $importForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=tax;method=importTax')})
. WebGUI::Form::submit($session,{value=>$i18n->get('import tax','Shop'), extras=>q{style="float: left;"} })
. q{<input type="file" name="importFile" size="10" />}
. WebGUI::Form::formFooter($session);
return $session->privilege->insufficient unless $admin->canManage;
my $addForm = WebGUI::HTMLForm->new($session,action=>$url->page('shop=tax;method=addTax'));
$addForm->text(
label => $i18n->get('country'),
hoverHelp => $i18n->get('country help'),
name => 'country',
);
$addForm->text(
label => $i18n->get('state'),
hoverHelp => $i18n->get('state help'),
name => 'state',
);
$addForm->text(
label => $i18n->get('city'),
hoverHelp => $i18n->get('city help'),
name => 'city',
);
$addForm->text(
label => $i18n->get('code'),
hoverHelp => $i18n->get('code help'),
name => 'code',
);
$addForm->float(
label => $i18n->get('tax rate'),
hoverHelp => $i18n->get('tax rate help'),
name => 'taxRate',
);
$addForm->submit(
value => $i18n->get('add a tax'),
);
my $output;
if ($status_message) {
$output = <<EOSM;
<div class="error">
$status_message
</div>
EOSM
}
$output .= q|
<div class="yui-skin-sam">
<div id="search"><form id="keywordSearchForm"><input type="text" name="keywords" id="keywordsField" /><input type="submit" value="|.$i18n->get(364, 'WebGUI').q|" /></form></div>
<div id="dynamicdata"></div>
<div id="adding">|.$addForm->print.q|</div>
<div id="importExport">|.$exportForm.$importForm.q|</div>
</div>
my ($style, $url) = $session->quick( qw(style url) );
my $i18n = WebGUI::International->new( $session, 'Tax' );
<script type="text/javascript">
var taxtable = function() {
// Column definitions
formatDeleteTaxId = function(elCell, oRecord, oColumn, orderNumber) {
elCell.innerHTML = '<a href="|.$url->page(q{shop=tax;method=deleteTax}).q|;taxId='+oRecord.getData('taxId')+'">|.$i18n->get('delete').q|</a>';
};
var myColumnDefs = [ // sortable:true enables sorting
{key:"country", label:"|.$i18n->get('country').q|", sortable: true},
{key:"state", label:"|.$i18n->get('state').q|", sortable: true},
{key:"city", label:"|.$i18n->get('city').q|", sortable: true},
{key:"code", label:"|.$i18n->get('code').q|", sortable: true},
{key:"taxRate", label:"|.$i18n->get('tax rate').q|"},
{key:"taxId", label:"", formatter:formatDeleteTaxId}
];
// DataSource instance
var myDataSource = new YAHOO.util.DataSource("|.$url->page('shop=tax;method=getTaxesAsJson;').q|");
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.responseSchema = {
resultsList: "records",
fields: [
{key:"country", parser:"string"},
{key:"state", parser:"string"},
{key:"city", parser:"string"},
{key:"code", parser:"string"},
{key:"taxRate", parser:"number"},
{key:"taxId", parser:"string"}
],
metaFields: {
totalRecords: "totalRecords" // Access to value in the server response
}
};
// DataTable configuration
var myConfigs = {
initialRequest: 'startIndex=0;results=25', // Initial request for first page of data
dynamicData: true, // Enables dynamic server-driven data
sortedBy : {key:"country", dir:YAHOO.widget.DataTable.CLASS_ASC}, // Sets UI initial sort arrow
paginator: new YAHOO.widget.Paginator({ rowsPerPage:25 }) // Enables pagination
};
// DataTable instance
var myDataTable = new YAHOO.widget.DataTable("dynamicdata", myColumnDefs, myDataSource, myConfigs);
// Update totalRecords on the fly with value from server to allow pagination
myDataTable.handleDataReturnPayload = function(oRequest, oResponse, oPayload) {
oPayload.totalRecords = oResponse.meta.totalRecords;
return oPayload;
}
my $activePlugin = $session->setting->get( 'activeTaxPlugin' );
my $plugins = $session->config->get( 'taxDrivers' );
my %options = map { $_ => $_ } @{ $plugins };
//Setup the form to submit an AJAX request back to the site.
YAHOO.util.Dom.get('keywordSearchForm').onsubmit = function () {
var state = myDataTable.getState();
state.pagination.recordOffset = 0;
myDataSource.sendRequest('keywords=' + YAHOO.util.Dom.get('keywordsField').value + ';startIndex=0;results=25', {success: myDataTable.onDataReturnInitializeTable, scope:myDataTable, argument:state});
return false;
};
return {
ds: myDataSource,
dt: myDataTable
};
my $pluginSwitcher =
'<fieldset><legend>Active tax plugin</legend>'
. WebGUI::Form::formHeader( $session )
. WebGUI::Form::hidden( $session, { name => 'shop', value => 'tax' } )
. WebGUI::Form::hidden( $session, { name => 'method', value => 'setActivePlugin' } )
. 'Active Tax Plugin '
. WebGUI::Form::selectBox( $session, { name => 'className', value => $activePlugin, options => \%options } )
. WebGUI::Form::submit( $session, { value => 'Switch' } )
. WebGUI::Form::formFooter( $session )
. '</fieldset>'
;
# my $output;
# if ($status_message) {
# $output = qq{<div class="error">$status_message</div>};
# }
}();
my $taxDriver = $self->getDriver;
my $output =
$pluginSwitcher
. '<fieldset><legend>Plugin configuration</legend>'
. $taxDriver->getConfigurationScreen
. '</fieldset>'
;
</script>
|;
return $admin->getAdminConsole->render($output, $i18n->get('taxes', 'Shop'));
}
#-------------------------------------------------------------------
=head2 www_setActivePlugin ( )
Displays a warning that informs users that they're about to change the active taxing plugin. Includes a confirm and
cancel button.
=cut
sub www_setActivePlugin {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new( $session );
return $session->privilege->insufficient unless $admin->canManage;
my $message =
'Changing the active tax plugin will change the way tax is calulated on <b>all</b> products you sell. '
. 'Are you really sure you want to switch?';
my $proceedForm =
WebGUI::Form::formHeader( $session )
. WebGUI::Form::hidden( $session, { name => 'shop', value => 'tax' } )
. WebGUI::Form::hidden( $session, { name => 'method', value => 'setActivePluginConfirm' } )
. WebGUI::Form::hidden( $session, { name => 'className', value => $session->form->process('className') } )
. WebGUI::Form::submit( $session, { value => 'Proceed' } )
. WebGUI::Form::formFooter( $session );
my $cancelForm =
WebGUI::Form::formHeader( $session )
. WebGUI::Form::hidden( $session, { name => 'shop', value => 'tax' } )
. WebGUI::Form::hidden( $session, { name => 'method', value => 'manage' } )
. WebGUI::Form::submit( $session, { value => 'Cancel', extras => 'class="backwardButton"' } )
. WebGUI::Form::formFooter( $session );
my $output = $message . $proceedForm . $cancelForm;
return $admin->getAdminConsole->render( $output, 'Switch tax plugin' );
}
#-------------------------------------------------------------------
=head2 www_setActivePluginConfirm ( )
Actually changes the active tax driver.
=cut
sub www_setActivePluginConfirm {
my $self = shift;
my $session = $self->session;
my $admin = WebGUI::Shop::Admin->new( $session );
return $session->privilege->insufficient unless $admin->canManage;
my $className = $session->form->process( 'className', 'className' );
#### TODO: Check aginst list of available plugins.
$session->setting->set( 'activeTaxPlugin', $className );
return $self->www_manage;
}
1;

View file

@ -54,6 +54,12 @@ our $I18N = {
lastUpdated => 0,
},
'manage tax label' => {
message => q{Manage Tax Settings},
lastUpdated => 0,
context => q{Label for the manage tax tab},
},
};
1;

View file

@ -69,29 +69,7 @@ our $I18N = {
context => q|help for vendor field|
},
'override tax rate' => {
message => q|Override tax rate?|,
lastUpdated => 0,
context => q|A yes/no field asking whether to override tax rate.|
},
'override tax rate help' => {
message => q|Would you like to override the default tax rate for this item? Usually used in locales that have special or no tax on life essential items like food and clothing.|,
lastUpdated => 0,
context => q|help for override tax rate field|
},
'tax rate override' => {
message => q|Tax Rate Override|,
lastUpdated => 0,
context => q|a field containing the percentage to use to calculate tax for this item|
},
'tax rate override help' => {
message => q|What is the new percentage that should be used to calculate tax on this item?|,
lastUpdated => 0,
context => q|help for tax rate override field|
},
'add to cart' => {
message => q|Add To Cart|,

View file

@ -73,6 +73,31 @@ our $I18N = {
lastUpdated => 1206395083,
},
'override tax rate' => {
message => q|Override tax rate?|,
lastUpdated => 0,
context => q|A yes/no field asking whether to override tax rate.|
},
'override tax rate help' => {
message => q|Would you like to override the default tax rate for this item? Usually used in locales that have special or no tax on life essential items like food and clothing.|,
lastUpdated => 0,
context => q|help for override tax rate field|
},
'tax rate override' => {
message => q|Tax Rate Override|,
lastUpdated => 0,
context => q|a field containing the percentage to use to calculate tax for this item|
},
'tax rate override help' => {
message => q|What is the new percentage that should be used to calculate tax on this item?|,
lastUpdated => 0,
context => q|help for tax rate override field|
},
};
1;