diff --git a/lib/WebGUI/Content/Shop.pm b/lib/WebGUI/Content/Shop.pm index ed5775f60..70019ef61 100644 --- a/lib/WebGUI/Content/Shop.pm +++ b/lib/WebGUI/Content/Shop.pm @@ -21,6 +21,7 @@ use WebGUI::Shop::AddressBook; use WebGUI::Shop::Cart; use WebGUI::Shop::Credit; use WebGUI::Shop::Pay; +use WebGUI::Shop::Products; use WebGUI::Shop::Ship; use WebGUI::Shop::Tax; use WebGUI::Shop::Transaction; @@ -189,6 +190,27 @@ sub www_pay { #------------------------------------------------------------------- +=head2 www_products () + +Hand off to the tax system. + +=cut + +sub www_products { + my $session = shift; + my $output = undef; + my $method = "www_".$session->form->get("method"); + if ($method ne "www_" && WebGUI::Shop::Products->can($method)) { + $output = $WebGUI::Shop::Products->$method($session); + } + else { + WebGUI::Error::MethodNotFound->throw(error=>"Couldn't call non-existant method $method", method=>$method); + } + return $output; +} + +#------------------------------------------------------------------- + =head2 www_ship () Hand off to the shipper. diff --git a/lib/WebGUI/Shop/Products.pm b/lib/WebGUI/Shop/Products.pm index d9d60f2c0..c618e7942 100644 --- a/lib/WebGUI/Shop/Products.pm +++ b/lib/WebGUI/Shop/Products.pm @@ -141,26 +141,32 @@ sub importProducts { } ##Okay, if we got this far, then the data looks fine. return unless scalar @productData; - my $fetchProductId = $session->db->prepare('select assetId from Product where mastersku=? order by revisionDate DESC limit 1'); + my $fetchProductId = $session->db->prepare('select p.assetId from Product as p join sku as s on p.assetId=s.assetId and p.revisionDate=s.revisionDate where s.sku=? order by p.revisionDate DESC limit 1'); my $node = WebGUI::Asset::Sku::Product->getProductImportNode($session); + @headers = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @headers; PRODUCT: foreach my $productRow (@productData) { my %productRow; ##Order the data according to the headers, in whatever order they exist. @productRow{ @headers } = @{ $productRow }; - $fetchProductId->execute([$productRow->{mastersku}]); - my ($assetId) = $fetchProductId->hashRef->{assetId}; + $fetchProductId->execute([$productRow{mastersku}]); + my $asset = $fetchProductId->hashRef; ##If the assetId exists, we update data for it - if ($assetId) { + if ($asset->{assetId}) { + $session->log->warn("Modifying an existing product: $productRow{sku} = $asset->{assetId}\n"); + my $assetId = $asset->{assetId}; my $product = WebGUI::Asset->newPending($session, $assetId); if ($productRow{title} ne $product->getTitle) { $product->update({ title => $product->fixTitle($productRow{title}) }); } ##Error handling for locked assets + $session->log->warn("Product is locked") if $product->isLocked; + delete $productRow{ title }; + delete $productRow{ mastersku }; next PRODUCT if $product->isLocked; my $collaterals = $product->getAllCollateral('variantsJSON'); my $collateralSet = 0; ROW: foreach my $collateral (@{ $collaterals }) { - next ROW unless $collateral->{sku} eq $productRow->{sku}; + next ROW unless $collateral->{sku} eq $productRow{sku}; @{ $collateral}{@headers} = @productRow{ @headers }; $product->setCollateral('variantsJSON', 'variantId', $collateral->{variantId}, $collateral); $collateralSet=1; @@ -172,12 +178,96 @@ sub importProducts { } else { ##Insert a new product; + $session->log->warn("Making a new product: $productRow{sku}\n"); my $newProduct = $node->addChild({className => 'WebGUI::Asset::Sku::Product'}); - $newProduct->update({ title => $newProduct->fixTitle($productRow{title}) }); + $newProduct->update({ + title => $newProduct->fixTitle($productRow{title}), + sku => $productRow{mastersku}, + }); + delete $productRow{ title }; + delete $productRow{ mastersku }; $newProduct->setCollateral('variantsJSON', 'variantId', 'new', \%productRow); + $newProduct->commit; } } return 1; } +#------------------------------------------------------------------- + +=head2 www_exportProducts ( ) + +Export all product SKUs as a CSV file. Returns a WebGUI::Storage +object containg the product file, named 'siteProductData.csv'. + +=cut + +sub www_exportProducts { + my $self = shift; + my $session = $self->session; + my $admin = WebGUI::Shop::Admin->new($session); + return $session->privilege->insufficient + unless $admin->canManage; + my $storage = $self->exportProducts(); + $self->session->http->setRedirect($storage->getUrl($storage->getFiles->[0])); + return "redirect"; +} + +#------------------------------------------------------------------- + +=head2 www_importProducts ( ) + +Import new product data from a file provided by the user. This will create new products +or alter existing products. + +=cut + +sub www_importProducts { + 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 $productFile = $storage->addFileFromFormPost('importFile', 1); + $self->importProducts($storage->getPath($productFile)) if $productFile; + return $self->www_manage; +} + +#------------------------------------------------------------------- + +=head2 www_manage ( ) + +User interface to synchronize product data. Provides an interface for +exporting all products on the site, and importing sets of products. + +=cut + +sub www_manage { + my $self = 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)); + ##Default CSS + $style->setRawHeadTags(''); + my $i18n=WebGUI::International->new($session, 'Shop'); + + my $exportForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=products;method=exportProducts')}) + . WebGUI::Form::submit($session,{value=>$i18n->get('export'), extras=>q{style="float: left;"} }) + . WebGUI::Form::formFooter($session); + my $importForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=products;method=importProducts')}) + . WebGUI::Form::submit($session,{value=>$i18n->get('import'), extras=>q{style="float: left;"} }) + . q{} + . WebGUI::Form::formFooter($session); + + my $output =sprintf <%s%s +EODIV + + return $admin->getAdminConsole->render($output, $i18n->get('products')); +} + 1; diff --git a/lib/WebGUI/Shop/Tax.pm b/lib/WebGUI/Shop/Tax.pm index d21534724..ba4430699 100644 --- a/lib/WebGUI/Shop/Tax.pm +++ b/lib/WebGUI/Shop/Tax.pm @@ -529,10 +529,10 @@ sub www_manage { my $i18n=WebGUI::International->new($session, 'Tax'); my $exportForm = WebGUI::Form::formHeader($session,{action => $url->page('shop=tax;method=exportTax')}) - . WebGUI::Form::submit($session,{value=>$i18n->get('export'), extras=>q{style="float: left;"} }) + . WebGUI::Form::submit($session,{value=>$i18n->get('export','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'), extras=>q{style="float: left;"} }) + . WebGUI::Form::submit($session,{value=>$i18n->get('import','Shop'), extras=>q{style="float: left;"} }) . q{} . WebGUI::Form::formFooter($session); diff --git a/lib/WebGUI/i18n/English/Shop.pm b/lib/WebGUI/i18n/English/Shop.pm index fcb63bda6..8e5c52f42 100644 --- a/lib/WebGUI/i18n/English/Shop.pm +++ b/lib/WebGUI/i18n/English/Shop.pm @@ -8,379 +8,379 @@ our $I18N = { lastUpdated => 0, context => q|notice after purchase|, }, - + 'shop notice' => { message => q|Shop Notice|, lastUpdated => 0, context => q|an email subject heading for generic shop notification emails|, }, - + 'mixed items warning' => { message => q|You are not able to check out with both recurring and non-recurring items in your cart. You may have either one recurring item, or as many non-recurring items as you want in your cart at checkout time. If you need to purchase both, then please purchase them under separate transactions.|, lastUpdated => 0, context => q|a warning message displayed in the cart|, }, - + 'cancel recurring transaction' => { message => q|Cancel Recurring Transaction|, lastUpdated => 0, context => q|a link label|, }, - + 'print' => { message => q|Print|, lastUpdated => 0, context => q|a link label|, }, - + 'minicart template' => { message => q|MiniCart Template|, lastUpdated => 0, context => q|a help title|, }, - + 'address book template' => { message => q|Address Book Template|, lastUpdated => 0, context => q|a help title|, }, - + 'cart template' => { message => q|Cart Template|, lastUpdated => 0, context => q|a help title|, }, - + 'address book template' => { message => q|Address Book Template|, lastUpdated => 0, context => q|a help title|, }, - + 'quantity help' => { message => q|The number of this item that is purchased.|, lastUpdated => 0, context => q|a help description|, }, - + 'item name help' => { message => q|The name or title of the product.|, lastUpdated => 0, context => q|a help description|, }, - + 'price help' => { message => q|The amount this items costs to purchase.|, lastUpdated => 0, context => q|a help description|, }, - + 'configuredTitle help' => { message => q|The name of the item as configured for purchase.|, lastUpdated => 0, context => q|a help description|, }, - + 'dateAdded help' => { message => q|The date and time this item was added to the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'isUnique help' => { message => q|A condition indicating whether this item is unique and therefore can only have a quantity of 1.|, lastUpdated => 0, context => q|a help description|, }, - + 'quantityField help' => { message => q|The field where the user may specify the quantity of the item they wish to purchase.|, lastUpdated => 0, context => q|a help description|, }, - + 'isShippable help' => { message => q|A condition indicating whether the item can have a shipping address attached to it.|, lastUpdated => 0, context => q|a help description|, }, - + 'extendedPrice help' => { message => q|The result of price multipled by quantity.|, lastUpdated => 0, context => q|a help description|, }, - + 'removeButton help' => { message => q|Clicking this button will remove the item from the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'item shipToButton help' => { message => q|Clicking this button will set an alternate address as the destination of this item.|, lastUpdated => 0, context => q|a help description|, }, - + 'shippingAddress help' => { message => q|The HTML formatted address to ship to.|, lastUpdated => 0, context => q|a help description|, }, - + 'error help' => { message => q|If there are any problems the error message will be displayed here.|, lastUpdated => 0, context => q|a help description|, }, - + 'formHeader help' => { message => q|The top of the form.|, lastUpdated => 0, context => q|a help description|, }, - + 'formFooter help' => { message => q|The bottom of the form.|, lastUpdated => 0, context => q|a help description|, }, - + 'checkoutButton help' => { message => q|The button the user pushes to choose a payment method.|, lastUpdated => 0, context => q|a help description|, }, - + 'continueShoppingButton help' => { message => q|Clicking this button will take the user back to the site.|, lastUpdated => 0, context => q|a help description|, }, - + 'updateButton help' => { message => q|Clicking this button will apply the changes you made to the cart and recalculate all the prices.|, lastUpdated => 0, context => q|a help description|, }, - + 'chooseShippingButton help' => { message => q|Clicking this button will let the user pick a shipping address from the address book.|, lastUpdated => 0, context => q|a help description|, }, - + 'shipToButton help' => { message => q|Does the same as the chooseShippingButton.|, lastUpdated => 0, context => q|a help description|, }, - + 'subtotalPrice help' => { message => q|The price of all the items in the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'shippingPrice help' => { message => q|The price of shipping on all the items in the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'tax help' => { message => q|The price of tax on all the items in the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'hasShippingAddress help' => { message => q|A condition indicating whether the the user has already specified a shipping address. Shipping address is always required in order to calculate taxes.|, lastUpdated => 0, context => q|a help description|, }, - + 'shippingOptions help' => { message => q|A select list containing all the configured shipping options for this order.|, lastUpdated => 0, context => q|a help description|, }, - + 'inShopCreditAvailable help' => { message => q|The amount of in-shop credit the user has.|, lastUpdated => 0, context => q|a help description|, }, - + 'inShopCreditDeduction help' => { message => q|The amount of in-shop credit that has been applied to this order.|, lastUpdated => 0, context => q|a help description|, }, - + 'totalPrice help' => { message => q|The total checkout price of the cart as it stands currently.|, lastUpdated => 0, context => q|a help description|, }, - + 'totalItems help' => { message => q|The total number of items in the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'item url help' => { message => q|The url to view this item as configured.|, lastUpdated => 0, context => q|a help description|, }, - + 'items loop help' => { message => q|A loop containing the variables of each item in the cart.|, lastUpdated => 0, context => q|a help description|, }, - + 'minicart template help' => { message => q|The following variables are available in the template for the MiniCart macro.|, lastUpdated => 0, context => q|a help description|, }, - + 'cart template help' => { message => q|This template determines what the shopping cart looks like.|, lastUpdated => 0, context => q|a help description|, }, - + 'address book template help' => { message => q|This template determines what the address book will look like.|, lastUpdated => 0, context => q|a help description|, }, - + 'who can manage help' => { message => q|The group that has management rights over commerce.|, lastUpdated => 0, context => q|a help description|, }, - + 'who can manage' => { message => q|Who can manage?|, lastUpdated => 0, context => q|a setting|, }, - + 'address loop help' => { message => q|A loop containing the list of addresses in this book and their management tools.|, lastUpdated => 0, context => q|a help description|, }, - + 'address help' => { message => q|An HTML formatted address.|, lastUpdated => 0, context => q|a help description|, }, - + 'editButton help' => { message => q|A button that will allow the user to edit an existing address.|, lastUpdated => 0, context => q|a help description|, }, - + 'deleteButton help' => { message => q|A button that will allow the user to delete an existing address.|, lastUpdated => 0, context => q|a help description|, }, - + 'useButton help' => { message => q|A button that will allow the user to select an existing address for use on a form.|, lastUpdated => 0, context => q|a help description|, }, - + 'addButton help' => { message => q|A button that will allow the user to add a new address.|, lastUpdated => 0, context => q|a help description|, }, - + 'saveButton help' => { message => q|The default save button for the form.|, lastUpdated => 0, context => q|a help description|, }, - + 'address1Field help' => { message => q|The field for the main address line.|, lastUpdated => 0, context => q|a help description|, }, - + 'address2Field help' => { message => q|The field for the secondary address line.|, lastUpdated => 0, context => q|a help description|, }, - + 'address3Field help' => { message => q|The field for the tertiary address line.|, lastUpdated => 0, context => q|a help description|, }, - + 'address labelField help' => { message => q|A field to contain the address label like 'home' or 'work'.|, lastUpdated => 0, context => q|a help description|, }, - + 'address nameField help' => { message => q|A field to contain the name of the person/company for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'cityField help' => { message => q|A field to contain the city for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'stateField help' => { message => q|A field to contain the state or province for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'countryField help' => { message => q|A field to contain the country for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'codeField help' => { message => q|A field to contain the zip code or postal code for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'phoneNumberField help' => { message => q|A field to contain the phone number for this address.|, lastUpdated => 0, context => q|a help description|, }, - + 'view cart' => { message => q|View Cart|, lastUpdated => 0, context => q|a link label|, }, - + 'my purchases' => { message => q|My Purchases|, lastUpdated => 0, @@ -591,6 +591,12 @@ our $I18N = { context => q|admin function label| }, + 'products' => { + message => q|Products|, + lastUpdated => 0, + context => q|admin function label| + }, + 'is a required field' => { message => q|%s is a required field.|, lastUpdated => 0, @@ -849,7 +855,7 @@ our $I18N = { context => q|Period name for a weekly subscription.| }, - + 'biweekly' => { message => q|Two weeks|, lastUpdated => 0, @@ -884,13 +890,24 @@ our $I18N = { context => q|Period name for a semi yearly subscription.| }, - + 'yearly' => { message => q|Year|, lastUpdated => 0, context => q|Period name for a yearly subscription.| }, + 'import' => { + message => q|Import|, + lastUpdated => 1212550974, + context => q|Label for bringing data into the Shop (Tax, Product, etc.)| + }, + + 'export' => { + message => q|Export|, + lastUpdated => 1212550978, + context => q|Label for taking data out of the Shop (Tax, Product, etc.)|, + }, }; 1; diff --git a/lib/WebGUI/i18n/English/Tax.pm b/lib/WebGUI/i18n/English/Tax.pm index 7fa8c17fc..97af278d4 100644 --- a/lib/WebGUI/i18n/English/Tax.pm +++ b/lib/WebGUI/i18n/English/Tax.pm @@ -33,18 +33,6 @@ our $I18N = { context => q|The amount that a person is charged to buy something, a percentage of the price.|, }, - 'export' => { - message => q|Export|, - lastUpdated => 1206307669, - context => q|To ship a copy of the tax data out of the server.|, - }, - - 'import' => { - message => q|Import|, - lastUpdated => 1206390280, - context => q|To bring in new tax data that replaces the current data.|, - }, - 'delete' => { message => q|delete|, lastUpdated => 1206385749, diff --git a/t/Shop/Products.t b/t/Shop/Products.t index 0fda98a88..c7132cec0 100644 --- a/t/Shop/Products.t +++ b/t/Shop/Products.t @@ -33,7 +33,7 @@ my $session = WebGUI::Test->session; #---------------------------------------------------------------------------- # Tests -my $tests = 14; +my $tests = 22; plan tests => 1 + $tests; #---------------------------------------------------------------------------- @@ -134,9 +134,68 @@ SKIP: { 'importProducts: error handling for a file with a missing header', ); + my $pass = WebGUI::Shop::Products::importProducts( + $session, + WebGUI::Test->getTestCollateralPath('productTables/goodProductTable.csv'), + ); + ok($pass, 'Products imported'); + + my $count = $session->db->quickScalar('select count(*) from Product'); + is($count, 2, 'two products were imported'); + + my $soda = WebGUI::Asset::Sku->newBySku($session, 'soda'); + isa_ok($soda, 'WebGUI::Asset::Sku::Product'); + is($soda->getTitle(), 'Sweet Soda-bottled in Oregon', 'Title set correctly for soda'); + my $sodaCollateral = $soda->getAllCollateral('variantsJSON'); + cmp_deeply( + $sodaCollateral, + [ + { + sku => 'soda-sweet', + shortdesc => 'Sweet Soda', + price => 0.95, + weight => 0.95, + quantity => 500, + variantId => ignore(), + }, + ], + 'collateral set correctly for soda' + ); + + my $shirt = WebGUI::Asset::Sku->newBySku($session, 't-shirt'); + isa_ok($shirt, 'WebGUI::Asset::Sku::Product'); + is($shirt->getTitle(), 'Colored T-Shirts', 'Title set correctly for t-shirt'); + my $shirtCollateral = $shirt->getAllCollateral('variantsJSON'); + cmp_deeply( + $shirtCollateral, + [ + { + sku => 'red-t-shirt', + shortdesc => 'Red T-Shirt', + price => '5.00', + weight => '1.33', + quantity => '1000', + variantId => ignore(), + }, + { + sku => 'blue-t-shirt', + shortdesc => 'Blue T-Shirt', + price => '5.25', + weight => '1.33', + quantity => '2000', + variantId => ignore(), + }, + ], + 'collateral set correctly for shirt' + ); + } #---------------------------------------------------------------------------- # Cleanup END { + my $getAProduct = WebGUI::Asset::Sku::Product->getIsa($session); + while (my $product = $getAProduct->()) { + $product->purge; + } } diff --git a/t/supporting_collateral/productTables/goodProductTable.csv b/t/supporting_collateral/productTables/goodProductTable.csv index 407252bfe..8107938f2 100644 --- a/t/supporting_collateral/productTables/goodProductTable.csv +++ b/t/supporting_collateral/productTables/goodProductTable.csv @@ -1,4 +1,4 @@ mastersku,sku,title,shortdescription,price,weight,quantity -t-shirt,red-t-shirt,Red T-Shirt,Red T-Shirt,5.00,1.33,1000 -t-shirt,blue-t-shirt,Blue T-Shirt,Blue T-Shirt,5.25,1.33,2000 +t-shirt,red-t-shirt,Colored T-Shirts,Red T-Shirt,5.00,1.33,1000 +t-shirt,blue-t-shirt,Colored T-Shirts,Blue T-Shirt,5.25,1.33,2000 soda,soda-sweet,Sweet Soda-bottled in Oregon,Sweet Soda,0.95,0.95,500