package WebGUI::Shop::TaxDriver::EU; =head1 LEGAL ------------------------------------------------------------------- WebGUI is Copyright 2001-2009 Plain Black Corporation. ------------------------------------------------------------------- Please read the legal notices (docs/legal.txt) and the license (docs/license.txt) that came with this distribution before using this software. ------------------------------------------------------------------- http://www.plainblack.com info@plainblack.com ------------------------------------------------------------------- =cut use strict; use SOAP::Lite; use WebGUI::Content::Account; use WebGUI::TabForm; use WebGUI::Utility qw{ isIn }; use base qw{ WebGUI::Shop::TaxDriver }; =head1 NAME Package WebGUI::Shop::TaxDriver::EU =head1 DESCRIPTION This package manages tax information, and calculates taxes on a shopping cart specifically handling European Union VAT taxes. =head1 SYNOPSIS use WebGUI::Shop::Tax; my $tax = WebGUI::Shop::Tax->new($session); =head1 METHODS These subroutines are available from this package: =cut my $EU_COUNTRIES = { AT => 'Austria', BE => 'Belgium', BG => 'Bulgaria', CY => 'Cyprus', CZ => 'Czech Republic', DE => 'Germany', DK => 'Denmark', EE => 'Estonia', EL => 'Greece', ES => 'Spain', FI => 'Finland', FR => 'France ', GB => 'United Kingdom', HU => 'Hungary', IE => 'Ireland', IT => 'Italy', LT => 'Lithuania', LU => 'Luxembourg', LV => 'Latvia', MT => 'Malta', NL => 'Netherlands', PL => 'Poland', PT => 'Portugal', RO => 'Romania', SE => 'Sweden', SI => 'Slovenia', SK => 'Slovakia', }; #------------------------------------------------------------------- =head2 className Returns the name of this class. =cut sub className { return 'WebGUI::Shop::TaxDriver::EU'; } #----------------------------------------------------------- =head2 getConfigurationScreen ( ) Returns the form that contains the configuration options for this plugin in the admin console. =cut sub getConfigurationScreen { my $self = shift; my $session = $self->session; my $taxGroups = $self->get( 'taxGroups' ) || []; # General setting form my $f = WebGUI::HTMLForm->new( $session ); $f->hidden( name => 'shop', value => 'tax', ); $f->hidden( name => 'method', value => 'do', ); $f->hidden( name => 'do', value => 'saveConfiguration', ); $f->selectBox( name => 'shopCountry', value => $self->get( 'shopCountry' ), label => 'Residential country', hoverHelp => 'The country where your shop resides.', options => $EU_COUNTRIES, ); $f->submit; my $general = $f->print; # VAT groups manager my $vatGroups = 'VAT groups
'; $vatGroups .= q{}; foreach my $group ( @{ $taxGroups} ) { my $deleteUrl = $session->url->page('shop=tax;method=do;do=deleteGroup;groupId=' . $group->{ id }); my $makeDefaultUrl = $session->url->page('shop=tax;method=do;do=setDefaultGroup;groupId=' . $group->{ id }); $vatGroups .= q{}; } $vatGroups .= q{
Group nameRate
} . join( '', $group->{ name } . ( $group->{ id } eq $self->get( 'defaultGroup' ) ? '(default)' : '' ), $group->{ rate }, qq{delete}, qq{Set as default group}, ) . q{
}; $vatGroups .= WebGUI::Form::formHeader( $session ) . WebGUI::Form::hidden( $session, { name => 'shop', value => 'tax' } ) . WebGUI::Form::hidden( $session, { name => 'method', value => 'do' } ) . WebGUI::Form::hidden( $session, { name => 'do', value => 'addGroup' } ) . 'Name ' . WebGUI::Form::text( $session, { name => 'name' } ) . ' Rate ' . WebGUI::Form::float( $session, { name => 'rate' } ) . '%' . WebGUI::Form::submit( $session, { value => 'Add' } ) . WebGUI::Form::formFooter( $session ); # Wrap output in a YUI Tab widget. my ($style, $url) = $session->quick( qw{ style url } ); $style->setLink($self->{_css},{rel=>"stylesheet", rel=>"stylesheet",type=>"text/css"}); $style->setLink($url->extras('/yui/build/fonts/fonts-min.css'),{type=>"text/css", rel=>"stylesheet"}); $style->setLink($url->extras('/yui/build/tabview/assets/skins/sam/tabview.css'),{type=>"text/css", rel=>"stylesheet"}); $style->setLink($url->extras('/yui/build/container/assets/container.css'),{ type=>'text/css', rel=>"stylesheet" }); $style->setLink($url->extras('/hoverhelp.css'),{ type=>'text/css', rel=>"stylesheet" }); $style->setScript($url->extras('/yui/build/utilities/utilities.js'),{ type=>'text/javascript' }); $style->setScript($url->extras('/yui/build/container/container-min.js'),{ type=>'text/javascript' }); $style->setScript($url->extras('/yui/build/tabview/tabview-min.js'),{ type=>'text/javascript' }); $style->setScript($url->extras('/hoverhelp.js'),{ type=>'text/javascript' }); my $output = <
$general
$vatGroups
EOHTML return $output; } #------------------------------------------------------------------- =head2 getCountryCode ($countryName) Given a country name, return a 2 character country code. =head3 $countryName The name of the country to look up. =cut sub getCountryCode { my $self = shift; my $countryName = shift; # Do reverse lookup on eu countries hash return { reverse %{ $EU_COUNTRIES } }->{ $countryName }; } #------------------------------------------------------------------- =head2 getCountryName ($countryCode) Given a 2 character country code, return the name of the country. =head3 $countryCode The code of the country to look up. =cut sub getCountryName { my $self = shift; my $countryCode = shift; return $EU_COUNTRIES->{ $countryCode }; } #------------------------------------------------------------------- =head2 getGroupRate ($taxGroupId) =head3 $taxGroupId =cut sub getGroupRate { my $self = shift; my $taxGroupId = shift; my $taxGroups = $self->get( 'taxGroups' ); my ($group) = grep { $_->{ id } eq $taxGroupId } @{ $taxGroups }; return $group->{ rate }; } #------------------------------------------------------------------- =head2 getUserScreen ( ) Returns the screen for entering per user configuration for this tax driver. =cut sub getUserScreen { my $self = shift; my $url = $self->session->url; my $output = 'VAT Numbers
' . ''; foreach my $number ( @{ $self->getVATNumbers } ) { my $deleteUrl = $url->page('shop=tax;method=do;do=deleteVATNumber;vatNumber='.$number->{ vatNumber }); $output .= '' ; } $output .= '
CountryVAT Number
' . join( '', $self->getCountryName( $number->{ countryCode } ), $number->{ vatNumber }, $number->{ name }, $number->{ address }, $number->{ approved }, qq{delete}, ) . '
'; my $f = WebGUI::HTMLForm->new( $self->session ); $f->hidden( name => 'shop', value => 'tax', ); $f->hidden( name => 'method', value => 'do', ); $f->hidden( name => 'do', value => 'addVATNumber', ); $f->text( name => 'vatNumber', label => 'VAT Number', ); $f->submit( value => 'Add', ); $output .= $f->print; return $output; } #------------------------------------------------------------------- =head2 getTaxRate ( sku, [ address ] ) Returns the tax rate in percents (eg. 19 for a rate of 19%) for the given sku and shipping address. Implements EU VAT taxes and group rates. =cut sub getTaxRate { my $self = shift; my $sku = shift; my $address = shift; my $config = $sku->getTaxConfiguration( $self->className ); # Fetch the tax group from the sku. If the sku has none, use the default tax group. my $taxGroupId = $config->{ taxGroup } || $self->get( 'defaultGroup' ); my $taxRate = $self->getGroupRate( $taxGroupId ); # No shipping address yet. Return group tax rate. return $taxRate unless defined $address; # Shipping address outside EU? That means exporting so no VAT. my $country = $self->getCountryCode( $address->get( 'country' ) ); return 0 unless defined $country; # Shipping address in same country as shop? Pay VAT; return $taxRate if $country eq $self->get('shopCountry'); # Customer has VAT number in shipping country? Exempt from paying VAT. return 0 if $self->hasVATNumber( $country ); # Customer has no VAT number and resides in EU. Pay VAT; return $taxRate; } #------------------------------------------------------------------- =head2 getVATNumbers ($countryCode) =head3 $countryCode =cut sub getVATNumbers { my $self = shift; my $countryCode = shift; my $session = $self->session; my $sql = 'select * from tax_eu_vatNumbers where userId=?'; my $placeHolders = [ $session->user->userId ]; if ( $countryCode ) { $sql .= ' and countryCode=?'; push @{ $placeHolders }, $countryCode; } my $numbers = $session->db->buildArrayRefOfHashRefs( $sql, $placeHolders ); return $numbers; } #------------------------------------------------------------------- =head2 hasVATNumber ($countrycode) =head3 $countryCode =cut sub hasVATNumber { my $self = shift; my $countryCode = shift; my $numbers = $self->getVATNumbers( $countryCode ); return 0 unless @{ $numbers }; return $numbers->[0]->{ approved }; } #------------------------------------------------------------------- =head2 skuFormDefinition ( ) Returns a hash ref containing the form defintion for the per sku options for this tax driver. =cut sub skuFormDefinition { my $self = shift; my $taxGroups = $self->get( 'taxGroups' ); # If no tax groups are defined there's no need to add a form element. return {} unless $taxGroups; my %options = map { $_->{ id } => "$_->{ name } ($_->{ rate } \%)" } @{ $taxGroups }; tie my %definition, 'Tie::IxHash', ( taxGroup => { fieldType => 'selectBox', label => 'Tax group', options => \%options, } ); return \%definition; } #------------------------------------------------------------------- =head2 www_addGroup =cut sub www_addGroup { my $self = shift; my $form = $self->session->form; return $self->session->privilege->insufficient unless $self->canManage; my $groups = $self->get( 'taxGroups' ) || []; my $name = $form->process( 'name' ); my $rate = $form->process( 'rate' ); my $id = $self->session->id->generate; push @{ $groups }, { name => $name, rate => $rate, id => $id, }; $self->update( { taxGroups => $groups } ); return ''; } #------------------------------------------------------------------- =head2 www_addVATNumber =cut sub www_addVATNumber { my $self = shift; my $session = $self->session; my ($db, $form) = $session->quick( qw{ db form } ); return $session->privilege->insufficient if $session->user->isVisitor; my $vatNumber = uc $form->process( 'vatNumber' ); my ($countryCode, $number) = $vatNumber =~ m/^([A-Z]{2})([A-Z0-9]+)$/; return 'Illegal country code' unless isIn( $countryCode, keys %{ $EU_COUNTRIES } ); return 'You already have a VAT number for this country.' if @{ $self->getVATNumbers( $countryCode ) }; # Check VAT number via SOAP interface. # TODO: Handle timeouts. my $soap = SOAP::Lite->service('http://ec.europa.eu/taxation_customs/vies/api/checkVatPort?wsdl'); my $isValid = ( $soap->checkVat( $countryCode, $number ) )[ 3 ] || 0; # Write the code to the db. $db->write( 'replace into tax_eu_vatNumbers (userId,countryCode,vatNumber,approved) values (?,?,?,?)', [ $self->session->user->userId, $countryCode, $vatNumber, $isValid, ] ); my $instance = WebGUI::Content::Account->createInstance($session,"shop"); return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); } #------------------------------------------------------------------- =head2 www_deleteGroup =cut sub www_deleteGroup { my $self = shift; my $form = $self->session->form; return $self->session->privilege->insufficient unless $self->canManage; my $taxGroups = $self->get( 'taxGroups' ); my $removeGroupId = $form->process( 'groupId' ); my @newGroups = grep { $_->{ id } ne $removeGroupId } @{ $taxGroups }; $self->update( { taxGroups => \@newGroups } ); return ''; } #------------------------------------------------------------------- =head2 www_deleteVATNumber =cut sub www_deleteVATNumber { my $self = shift; my $session = $self->session; return $session->privilege->insufficient unless $session->user->isVisitor; $session->db->write( 'delete from tax_eu_vatNumbers where userId=? and vatNumber=?', [ $session->user->userId, $session->form->process( 'vatNumber' ), ] ); my $instance = WebGUI::Content::Account->createInstance($session,"shop"); return $instance->displayContent( $instance->callMethod("manageTaxData", [], $session->user->userId) ); } #------------------------------------------------------------------- =head2 www_saveConfiguration =cut sub www_saveConfiguration { my $self = shift; my $form = $self->session->form; return $self->session->privilege->insufficient unless $self->canManage; $self->update( { shopCountry => $form->process( 'shopCountry', 'selectBox' ), } ); return ''; } #------------------------------------------------------------------- =head2 www_setDefaultGroup =cut sub www_setDefaultGroup { my $self = shift; my $form = $self->session->form; return $self->session->privilege->insufficient unless $self->canManage; $self->update( { defaultGroup => $form->process( 'groupId' ), } ); return ''; } 1;