diff --git a/lib/WebGUI/Asset/Wobject/Shelf.pm b/lib/WebGUI/Asset/Wobject/Shelf.pm index 717171ec0..80ef1c792 100644 --- a/lib/WebGUI/Asset/Wobject/Shelf.pm +++ b/lib/WebGUI/Asset/Wobject/Shelf.pm @@ -32,200 +32,31 @@ property templateId => ( namespace => "Shelf", hoverHelp => ['shelf template help', 'Asset_Shelf'], label => ['shelf template', 'Asset_Shelf'], - ); +); -#------------------------------------------------------------------- +#---------------------------------------------------------------------------- -=head2 exportProducts ( ) +=head2 getHelpers ( ) -Export all products from the WebGUI system in a CSV file. For details -about the file format, see importProducts. - -Returns a temporary WebGUI::Storage object containing the file. The -file will be named siteProductData.csv. +Add the importCSV and exportCSV helpers to the Shelf =cut -sub exportProducts { - my $self = shift; - my $session = $self->session; - my @columns = qw{varSku shortdescription price weight quantity}; - my $productData = WebGUI::Text::joinCSV(qw{mastersku title}, @columns) . "\n"; - @columns = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @columns; - my $getAProduct = WebGUI::Asset::Sku::Product->getIsa($session); - while (my $product = $getAProduct->()) { - my $mastersku = $product->sku; - my $title = $product->getTitle; - my $collateri = $product->getAllCollateral('variantsJSON'); - foreach my $collateral (@{ $collateri }) { - my @productFields = @{ $collateral }{ @columns }; - $productData .= WebGUI::Text::joinCSV($mastersku, $title, @productFields); - $productData .= "\n"; - } - } - my $storage = WebGUI::Storage->createTemp($session); - $storage->addFileFromScalar('siteProductData.csv', $productData); - return $storage; -} +override getHelpers => sub { + my ( $self ) = @_; + my $helpers = super(); -#------------------------------------------------------------------- - -=head2 importProducts ( $filePath ) - -Import products into the WebGUI system. If the master sku of a product -exists in the system, it will be updated. If master skus do not exist, -they will be added. - -The first line of the file should contain only the name of the columns, -in any order. It may not contain comments. - -These are the column names, each is required: - -=over 4 - -=item * - -mastersku - -=item * - -varsku - -=item * - -title - -=item * - -shortdescription - -=item * - -price - -=item * - -weight - -=item * - -quantity - -=back - -The following lines will contain product 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. - -=cut - -sub importProducts { - my $self = shift; - my $filePath = shift; - my $session = $self->session; - 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; - $headers =~ tr/\r//d; - $headers =~ s/\bsku\b/varSku/; - 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 'mastersku-price-quantity-shortdescription-title-varSku-weight') - and (scalar @headers == 7); - - my @productData = (); - my $line = 1; - while (my $productRow = <$table>) { - chomp $productRow; - $productRow =~ tr/\r//d; - $productRow =~ s/\s*#.+$//; - next unless $productRow; - local $_; - my @productRow = WebGUI::Text::splitCSV($productRow); - WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line) - unless scalar @productRow == 7; - push @productData, [ @productRow ]; - } - - return unless scalar @productData; - - ##Okay, if we got this far, then the data looks fine. - 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 = $self; - @headers = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @headers; - my @collateralFields = grep { $_ ne 'title' and $_ ne 'mastersku' } @headers; - PRODUCT: foreach my $productRow (@productData) { - my %productRow; - ##Order the data according to the headers, in whatever order they exist. - @productRow{ @headers } = @{ $productRow }; - ##Isolate just the collateral from the other product information - my %productCollateral; - @productCollateral{ @collateralFields } = @productRow{ @collateralFields }; - - $fetchProductId->execute([$productRow{mastersku}]); - my $asset = $fetchProductId->hashRef; - - ##If the assetId exists, we update data for it - 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); - - ##Error handling for locked assets - if ($product->isLocked) { - $session->log->warn("Product is locked"); - next PRODUCT if $product->isLocked; - } - - if ($productRow{title} ne $product->getTitle) { - $product->update({ - title => $productRow{title}, - menuTitle => $productRow{title}, - }); - } - - my $collaterals = $product->getAllCollateral('variantsJSON'); - my $collateralSet = 0; - ROW: foreach my $collateral (@{ $collaterals }) { - next ROW unless $collateral->{varSku} eq $productRow{varSku}; - @{ $collateral}{ @collateralFields } = @productCollateral{ @collateralFields }; ##preserve the variant Id field, assign all others - $product->setCollateral('variantsJSON', 'variantId', $collateral->{variantId}, $collateral); - $collateralSet=1; - } - if (!$collateralSet) { - ##It must be a new variant - $product->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral); - } - } - 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 => $productRow{title}, - menuTitle => $productRow{title}, - url => $productRow{title}, - sku => $productRow{mastersku}, - }); - $newProduct->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral); - $newProduct->commit; - } - } - return 1; -} + $helpers->{import_products} = { + className => 'WebGUI::AssetHelper::Product::ImportCSV', + label => 'Import Products', + }; + $helpers->{export_products} = { + className => 'WebGUI::AssetHelper::Product::ExportCSV', + label => 'Export Products', + }; + return $helpers; +}; #------------------------------------------------------------------- @@ -331,102 +162,5 @@ sub view { return $self->processTemplate(\%var, undef, $self->{_viewTemplate}); } - -#------------------------------------------------------------------- - -=head2 www_edit ( ) - -Override the superclass to add import and exprt items to the AdminConsole submenu. - -=cut - -override www_edit => sub { - my $self = shift; - my $i18n = WebGUI::International->new($self->session, 'Asset_Shelf'); - if ($self->getId ne "new") { - $self->getAdminConsole->addSubmenuItem($self->getUrl('func=exportProducts'),$i18n->get("export")); - $self->getAdminConsole->addSubmenuItem($self->getUrl('func=importProducts'),$i18n->get("import")); - } - return super(); -}; - -#------------------------------------------------------------------- - -=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; - return $session->privilege->insufficient - unless $self->canEdit; - my $storage = $self->exportProducts(); - $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; - return $session->privilege->insufficient unless $self->canEdit; - my $i18n=WebGUI::International->new($session, 'Asset_Shelf'); - my $status_message; - if ( $session->form->get('doit')) { - my $storage = WebGUI::Storage->create($session); - my $productFile = $storage->addFileFromFormPost('importFile', 1); - eval { - $self->importProducts($storage->getPath($productFile)) if $productFile; - }; - my $exception; - 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; - } - } - elsif ($exception = Exception::Class->caught()) { - $status_message = sprintf 'A problem happened during the import: %s', $exception->error; - } - else { - $status_message = $i18n->get('import successful'); - if (WebGUI::VersionTag->autoCommitWorkingIfEnabled($self->session, { - allowComments => 1, - returnUrl => $self->getUrl, - }) eq 'redirect') { - return undef; - }; - } - } - - my $output; - if ($status_message) { - $output = '
'.$status_message.'
'; - } - - $output .= WebGUI::Form::formHeader($session,{action => $self->getUrl}) - . WebGUI::Form::hidden($session, {name=>"func", value=>"importProducts"}) - . WebGUI::Form::hidden($session, {name=>"doit", value=>1}) - . q{} - . WebGUI::Form::submit($session,{value=>$i18n->get('import'), extras=>q{style="float: left;"} }) - . WebGUI::Form::formFooter($session); - - return $self->getAdminConsole->render($output, $i18n->get('import')); -} - - __PACKAGE__->meta->make_immutable; 1; diff --git a/lib/WebGUI/AssetHelper/Product/ExportCSV.pm b/lib/WebGUI/AssetHelper/Product/ExportCSV.pm new file mode 100644 index 000000000..b9be0d258 --- /dev/null +++ b/lib/WebGUI/AssetHelper/Product/ExportCSV.pm @@ -0,0 +1,95 @@ +package WebGUI::AssetHelper::Product::ExportCSV; + +use Moose; +extends 'WebGUI::AssetHelper'; + +use JSON; +use WebGUI::Asset::Sku::Product; +use WebGUI::Fork; +use WebGUI::Text; +use WebGUI::Asset; +use WebGUI::Storage; + +#------------------------------------------------------------------- + +=head2 process ( ) + +Fork the copy operation + +=cut + +sub process { + my ($self) = @_; + my $asset = $self->asset; + my $session = $self->session; + + # Fork the export. Forking makes sure it won't get interrupted + my $fork = WebGUI::Fork->start( + $session, blessed( $self ), 'exportProducts', + ); + + return { + forkId => $fork->getId, + }; +} + +#------------------------------------------------------------------- + +=head2 exportProducts ( ) + +Export all products from the WebGUI system in a CSV file. For details +about the file format, see WebGUI::AssetHelper::Product::ImportCSV + +Returns a temporary WebGUI::Storage object containing the file. The +file will be named siteProductData.csv. + +=cut + +sub exportProducts { + my ($process, $args) = @_; + my $session = $process->session; + + # Get all the product IDs + # Not using getIsa so I can have the number to put into the progress bar + # This should be perhaps genericized and placed into WebGUI::Asset + my $tableName = $session->db->dbh->quote_identifier( WebGUI::Asset::Sku::Product->tableName ); + my $productIds = $session->db->buildArrayRef( + "SELECT assetId FROM asset JOIN assetData USING (assetId) JOIN $tableName USING (assetId, revisionDate) WHERE status=? OR status=? HAVING MAX(revisionDate)", + ['approved','archived'], + ); + + # Preparing to dispense product + my $status = { + message => 'Dispensing product...', + total => scalar @{$productIds}, + finished => 0, + }; + $process->update( sub { JSON->new->encode( $status ) } ); + + # Dispensing product + my @columns = qw{varSku shortdescription price weight quantity}; + my $productData = WebGUI::Text::joinCSV(qw{mastersku title}, @columns) . "\n"; + @columns = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @columns; + for my $productId ( @$productIds ) { + my $product = WebGUI::Asset->newById( $session, $productId ); + my $mastersku = $product->sku; + my $title = $product->getTitle; + my $collateri = $product->getAllCollateral('variantsJSON'); + foreach my $collateral (@{ $collateri }) { + my @productFields = @{ $collateral }{ @columns }; + $productData .= WebGUI::Text::joinCSV($mastersku, $title, @productFields); + $productData .= "\n"; + } + $status->{finished}++; + $process->update( sub { JSON->new->encode( $status ) } ); + } + my $storage = WebGUI::Storage->createTemp($session); + $storage->addFileFromScalar('siteProductData.csv', $productData); + + # Are you still there? + $status->{redirect} = $storage->getUrl( 'siteProductData.csv' ); + $process->update( sub { JSON->new->encode( $status ) } ); + $session->log->info( "Products exported to " . $status->{redirect} ); +} + +1; diff --git a/lib/WebGUI/AssetHelper/Product/ImportCSV.pm b/lib/WebGUI/AssetHelper/Product/ImportCSV.pm new file mode 100644 index 000000000..12d4dbd71 --- /dev/null +++ b/lib/WebGUI/AssetHelper/Product/ImportCSV.pm @@ -0,0 +1,269 @@ +package WebGUI::AssetHelper::Product::ImportCSV; + +use Moose; +extends 'WebGUI::AssetHelper'; + +use PerlIO::eol; +use JSON; +use WebGUI::Exception; +use WebGUI::Fork; +use WebGUI::Text; +use WebGUI::Storage; +use WebGUI::International; + +#------------------------------------------------------------------- + +=head2 process ( ) + +Display a dialog to import products + +=cut + +sub process { + my ($self) = @_; + my $asset = $self->asset; + my $session = $self->session; + + return { + openDialog => $self->getUrl( 'importProducts' ), + }; +} + +#------------------------------------------------------------------- + +=head2 importProducts ( ) + +Import products into the WebGUI system. If the master sku of a product +exists in the system, it will be updated. If master skus do not exist, +they will be added. + +The first line of the file should contain only the name of the columns, +in any order. It may not contain comments. + +These are the column names, each is required: + +=over 4 + +=item * + +mastersku + +=item * + +varsku + +=item * + +title + +=item * + +shortdescription + +=item * + +price + +=item * + +weight + +=item * + +quantity + +=back + +The following lines will contain product 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. + +=cut + +sub importProducts { + my ( $process, $args ) = @_; + my $session = $process->session; + my $asset = WebGUI::Asset->newById( $session, $args->{assetId} ); + my $filePath = $args->{filePath}; + 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; + local $/ = "\x0A"; # Fork alters this!!! + open my $table, '<:raw:eol(CRLF)', $filePath or + WebGUI::Error->throw(error => qq{Unable to open $filePath for reading: $!\n}); + + # Read in the data + my $headers; + $headers = <$table>; + $session->log->info( "Headers: " . $headers ); + chomp $headers; + $headers =~ tr/\r//d; + $headers =~ s/\bsku\b/varSku/; + my @headers = WebGUI::Text::splitCSV($headers); + unless ( (join(q{-}, sort @headers) eq 'mastersku-price-quantity-shortdescription-title-varSku-weight') + and (scalar @headers == 7) ) { + $session->log->error( "Bad header found in CSV file ($filePath): $headers -- " . join ", ", sort @headers ); + WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath); + } + + my @productData = (); + my $line = 1; + while (my $productRow = <$table>) { + $session->log->info( "Product: " . $productRow ); + chomp $productRow; + $productRow =~ tr/\r//d; + $productRow =~ s/\s*#.+$//; + next unless $productRow; + local $_; + my @productRow = WebGUI::Text::splitCSV($productRow); + WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line) + unless scalar @productRow == 7; + push @productData, [ @productRow ]; + } + + if ( @productData == 0 ) { + $session->log->warn("No products to import"); + $process->update( sub { JSON->new->encode( { message => 'No products' } ) } ); + $process->finish; + return; + } + + # Preparing to load product + my $status = { + message => 'Loading product...', + total => scalar @productData, + finished => 0, + }; + $process->update( sub { JSON->new->encode( $status ) } ); + + ##Okay, if we got this far, then the data looks fine. + 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'); + @headers = map { $_ eq 'shortdescription' ? 'shortdesc' : $_ } @headers; + my @collateralFields = grep { $_ ne 'title' and $_ ne 'mastersku' } @headers; + PRODUCT: foreach my $productRow (@productData) { + my %productRow; + ##Order the data according to the headers, in whatever order they exist. + @productRow{ @headers } = @{ $productRow }; + ##Isolate just the collateral from the other product information + my %productCollateral; + @productCollateral{ @collateralFields } = @productRow{ @collateralFields }; + + $fetchProductId->execute([$productRow{mastersku}]); + my $asset = $fetchProductId->hashRef; + + ##If the assetId exists, we update data for it + 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); + + ##Error handling for locked assets + if ($product->isLocked) { + $session->log->warn("Product is locked"); + next PRODUCT if $product->isLocked; + } + + if ($productRow{title} ne $product->getTitle) { + $product->update({ + title => $productRow{title}, + menuTitle => $productRow{title}, + }); + } + my $collaterals = $product->getAllCollateral('variantsJSON'); + my $collateralSet = 0; + ROW: foreach my $collateral (@{ $collaterals }) { + next ROW unless $collateral->{varSku} eq $productRow{varSku}; + @{ $collateral}{ @collateralFields } = @productCollateral{ @collateralFields }; ##preserve the variant Id field, assign all others + $product->setCollateral('variantsJSON', 'variantId', $collateral->{variantId}, $collateral); + $collateralSet=1; + } + if (!$collateralSet) { + ##It must be a new variant + $product->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral); + } + } + else { + ##Insert a new product; + $session->log->warn("Making a new product: $productRow{sku}\n"); + my $newProduct = $asset->addChild({className => 'WebGUI::Asset::Sku::Product'}); + $newProduct->update({ + title => $productRow{title}, + menuTitle => $productRow{title}, + url => $productRow{title}, + sku => $productRow{mastersku}, + }); + $newProduct->setCollateral('variantsJSON', 'variantId', 'new', \%productCollateral); + $newProduct->commit; + } + + # Update our status + $status->{finished}++; + $process->update( sub { JSON->new->encode( $status ) } ); + } + + $process->finish; +} + +#----------------------------------------------------------------------------- + +=head2 www_importProducts ( ) + +Show the form to upload the CSV file + +=cut + +sub www_importProducts { + my $self = shift; + my $session = $self->session; + return $session->privilege->insufficient unless $self->asset->canEdit; + my $i18n = WebGUI::International->new( $session, 'Asset_Shelf' ); + + my $f = $self->getForm( 'importProductsSave' ); + $f->addField( 'file', name => 'importFile' ); + $f->addField( 'submit', name => 'submit', value => $i18n->get('import') ); + + return $session->style->process( + '

' . $i18n->get('import') . '

' . $f->toHtml, + "PBtmplBlankStyle000001" + ); +} + +#----------------------------------------------------------------------------- + +=head2 www_importProductsSave ( ) + +Import the products from the CSV file in a forked process + +=cut + +sub www_importProductsSave { + my ( $self ) = @_; + my $session = $self->session; + + my $storage = WebGUI::Storage->create($session); + my $productFile = $storage->addFileFromFormPost( 'importFile_file', 1 ); + + # Fork the import + my $fork = WebGUI::Fork->start( + $session, blessed( $self ), 'importProducts', + { assetId => $self->asset->getId, filePath => $storage->getPath( $productFile ), }, + ); + + my $output = '' + ; + return $session->style->process( + $output, + "PBtmplBlankStyle000001" + ); +} + +1;