Adding pluggable tax system.
This commit is contained in:
parent
eec6ac3e47
commit
2e4ce42b76
9 changed files with 366 additions and 590 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ( )
|
||||
|
|
|
|||
|
|
@ -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 ( )
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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|,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue