Sunday, July 22, 2012

Including Product Tags in nopCommerce Import/Export

Product tags are not included in Excel import/export by default in nopCommerce solution. Product tags are many-to-many relation to products. Following code illustrates how to import/export product tags in nopCommerce (added to Nop.Services.ExportImport in service layer):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nop.Core.Domain.Catalog;
using Nop.Services.Catalog;
using OfficeOpenXml;

namespace Nop.Services.ExportImport
{
    /// <summary>
    /// Import/Export helper class
    /// </summary>
    public class ImportExportHelper
    {
        public static string[] AddCustomColumns(string[] properties)
        {
            string[] newProperties = new string[] 
            {       
                "ProductTags",      
            };
            string[] returnProperties = properties.Union(newProperties).ToArray();
            return returnProperties;
        }

        public static string GetProductTagsString(Product product)
        {
            var result = new StringBuilder();
            for (int i = 0; i < product.ProductTags.Count; i++)
            {
                var pt = product.ProductTags.ToList()[i];
                result.Append(pt.Name);
                if (i != product.ProductTags.Count - 1)
                    result.Append(", ");
            }
            return result.ToString();
        }

        public static string[] ParseProductTags(string productTags)
        {
            var result = new List<string>();
            if (!String.IsNullOrWhiteSpace(productTags))
            {
                string[] values = productTags.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string val1 in values)
                    if (!String.IsNullOrEmpty(val1.Trim()))
                        result.Add(val1.Trim());
            }
            return result.ToArray();
        }

        public static void SaveProductTags(IProductService _productService, 
            IProductTagService _productTagService, Product product, string[] productTags)
        {
            if (product == null)
                throw new ArgumentNullException("product");

            //product tags
            var existingProductTags = product.ProductTags.OrderByDescending(pt => pt.ProductCount).ToList();
            var productTagsToDelete = new List<ProductTag>();
            foreach (var existingProductTag in existingProductTags)
            {
                bool found = false;
                foreach (string newProductTag in productTags)
                {
                    if (existingProductTag.Name.Equals(newProductTag, StringComparison.InvariantCultureIgnoreCase))
                    {
                        found = true;
                        break;
                    }
                }
                if (!found)
                {
                    productTagsToDelete.Add(existingProductTag);
                }
            }
            foreach (var productTag in productTagsToDelete)
            {
                product.ProductTags.Remove(productTag);
                //ensure product is saved before updating totals
                _productService.UpdateProduct(product);
                _productTagService.UpdateProductTagTotals(productTag);
            }
            foreach (string productTagName in productTags)
            {
                ProductTag productTag = null;
                var productTag2 = _productTagService.GetProductTagByName(productTagName);
                if (productTag2 == null)
                {
                    //add new product tag
                    productTag = new ProductTag()
                    {
                        Name = productTagName,
                        ProductCount = 0
                    };
                    _productTagService.InsertProductTag(productTag);
                }
                else
                {
                    productTag = productTag2;
                }
                if (!product.ProductTagExists(productTag.Id))
                {
                    product.ProductTags.Add(productTag);
                    //ensure product is saved before updating totals
                    _productService.UpdateProduct(product);
                }
                //update product tag totals 
                _productTagService.UpdateProductTagTotals(productTag);
            }
        }

    }

    /// <summary>
    /// Partial class of ExportManager
    /// </summary>
    public partial class ExportManager : IExportManager
    {
        public void UpdateCustomProductProperties(ExcelWorksheet worksheet, int row, ref int column, Product product)
        {
            string productTags = ImportExportHelper.GetProductTagsString(product);
            worksheet.Cells[row, column].Value = productTags;
            column++;
        }

    }

    /// <summary>
    /// Partial class of ImportManager
    /// </summary>
    public partial class ImportManager : IImportManager
    {
        public void UpdateCustomProductProperties(ExcelWorksheet worksheet, string[] properties, int row, Product product)
        {
            string productTags = Convert.ToString(worksheet.Cells[row, GetColumnIndex(properties, "ProductTags")].Value);
            ImportExportHelper.SaveProductTags(_productService, _productTagService, product, ImportExportHelper.ParseProductTags(productTags));
        }
    }
}
In order to use above code, the original ExportManager and ImportManager need to be modified a bit. Go to Nop.Services.ExportImport/ExportManger.cs, inside the ExportProductsToXlsx method:

1. Update property definition and add following code after line 532:
properties = ImportExportHelper.AddCustomColumns(properties);
2. Export product tags using custom function and add following code after line 797:
UpdateCustomProductProperties(worksheet, row, ref col, p);

Update Nop.Services.ExportImport/ExportManger.cs constructor and inject IProductTagService:
 public partial class ImportManager : IImportManager
    {
        private readonly IProductService _productService;
        private readonly ICategoryService _categoryService;
        private readonly IManufacturerService _manufacturerService;
        private readonly IPictureService _pictureService;
        private readonly IProductTagService _productTagService;

        public ImportManager(IProductService productService, ICategoryService categoryService,
            IManufacturerService manufacturerService, IPictureService pictureService, 
            IProductTagService productTagService)
        {
            this._productService = productService;
            this._categoryService = categoryService;
            this._manufacturerService = manufacturerService;
            this._pictureService = pictureService;
            this._productTagService = productTagService;
        }
Then update ImportProductsFromXlsx method inside ExportManger.cs:

1. Update property definition; add following code after line 142:
properties = ImportExportHelper.AddCustomColumns(properties);
2. Import product tags using custom function add following code after line 318 AND line 402:
UpdateCustomProductProperties(worksheet, properties, iRow, product);

Rebuild the solution and test out.

Create Custom Scheduled Task in nopCommerce

nopCommerce is a popular open source eCommerce solution built by ASP.NET MVC framework. We can create a custom scheduled task to scan the products periodically and do some business logic there, e.g. sending out email notification if some condition reached for certain products. In order to do that we need to define a record in the ScheduleTask table. Following script inserts a scheduled task record into database that would make nopCommerce invoke 'Nop.Services.Messages.ProductScanService' every 10 minutes:
INSERT INTO ScheduleTask(Name, Seconds, [Type], [Enabled], StopOnError)
 VALUES('ProductScanService schedule', 600, 'Nop.Services.Messages.ProductScanService', 1, 0) 
The ProductScanService class implements ITask interface, and is added to nop.Services.Messages in the service layer (tested in the latest version of 2.60):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nop.Services.Tasks;
using Nop.Services.Catalog;
using Nop.Services.Logging;

namespace Nop.Services.Messages
{
    class ProductScanService : ITask
    {
        private readonly IProductService _productService;
        private readonly ILogger _logger;

        /// <summary>
        /// Dependency injection here
        /// </summary>
        public ProductScanService(IProductService productService, ILogger logger)
        {
            this._productService = productService;
            this._logger = logger;
        }

        /// <summary>
        /// Executes the task
        /// </summary>
        public void Execute()
        {
            _logger.Information(string.Format("ProductScanService starts at {0} (UTC {1})", 
                DateTime.Now, DateTime.UtcNow));
            ScanAllProducts();
            _logger.Information(string.Format("ProductScanService ends at {0} (UTC {1})",
                DateTime.Now, DateTime.UtcNow));
        }

        /// <summary>
        /// Do business logic here
        /// </summary>
        private void ScanAllProducts()
        {
            // Do stuff with _productService
        }
    }
}