This commit is contained in:
Loretta 2025-10-15 08:09:13 +02:00
commit 4b7b558371
28 changed files with 6259 additions and 414 deletions

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components
{
[ViewComponent(Name = "FileUploadGridComponent")]
public class FileUploadGridComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(TestGridModel model)
{
// Here you can fetch data for this grid if needed
// For demo, just pass the model
return View(model.ViewComponentLocation, model);
}
}
}

View File

@ -0,0 +1,22 @@
// ViewComponent Class
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
using Nop.Services.Plugins;
using Org.BouncyCastle.Asn1.Ocsp;
using System.ComponentModel;
using System.Text.Json;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Components
{
[ViewComponent(Name = "TestGridComponent")]
public class TestGridComponent : ViewComponent
{
public IViewComponentResult Invoke(TestGridModel model)
{
return View(model.ViewComponentLocation, model);
}
}
}

View File

@ -1,25 +0,0 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.ShippingDocumentListModel
@using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
@using FruitBank.Common.Entities;
@using DevExtreme.AspNet.Mvc
<h4>Id: @Model.ShippingId</h4>
@(Html.DevExtreme().DataGrid<ShippingDocument>()
.ID("documentsGrid")
.DataSource(Model.ShippingDocumentList)
.KeyExpr("Id")
.ShowBorders(true)
.Editing(editing => {
editing.Mode(GridEditMode.Row);
editing.AllowUpdating(true);
editing.AllowAdding(false);
editing.AllowDeleting(true);
})
.Columns(c => {
c.AddFor(m => m.DocumentDate).Caption("Date").DataType(GridDataType.Date);
c.AddFor(m => m.SenderName).Caption("Sender");
c.AddFor(m => m.InvoiceNumber).Caption("Invoice #");
c.AddFor(m => m.TotalAmount).Caption("Amount").DataType(GridDataType.Number);
c.AddFor(m => m.ItemCount).Caption("ItemCount").DataType(GridDataType.Number);
})
)

View File

@ -9,6 +9,8 @@ using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Plugin.Misc.FruitBankPlugin.Factories;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Common;
using Nop.Services.Messages;
using Nop.Services.Orders;
using Nop.Services.Security;
using Nop.Web.Areas.Admin.Controllers;
@ -27,14 +29,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
private readonly CustomOrderModelFactory _orderModelFactory;
private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint;
private readonly IPermissionService _permissionService;
private readonly IGenericAttributeService _genericAttributeService;
private readonly INotificationService _notificationService;
// ... other dependencies
public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService)
public CustomOrderController(IOrderService orderService, IOrderModelFactory orderModelFactory, ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IPermissionService permissionService, IGenericAttributeService genericAttributeService, INotificationService notificationService)
{
_orderService = orderService;
_orderModelFactory = orderModelFactory as CustomOrderModelFactory;
_customOrderSignalREndpoint = customOrderSignalREndpoint;
_permissionService = permissionService;
_genericAttributeService = genericAttributeService;
_notificationService = notificationService;
// ... initialize other deps
}
@ -67,6 +73,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{
//prepare model
var orderListModel = await GetOrderListModelByFilter(searchModel);
//var orderListModel = new OrderListModel();
var valami = Json(orderListModel);
Console.WriteLine(valami);
@ -103,6 +110,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// return Json(model);
//}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveOrderAttributes(OrderAttributesModel model)
{
if (!ModelState.IsValid)
{
// reload order page with errors
return RedirectToAction("Edit", "Order", new { id = model.OrderId });
}
var order = await _orderService.GetOrderByIdAsync(model.OrderId);
if (order == null)
return RedirectToAction("List", "Order");
// store attributes in GenericAttribute table
await _genericAttributeService.SaveAttributeAsync(order, nameof(OrderModelExtended.IsMeasurable), model.IsMeasurable);
await _genericAttributeService.SaveAttributeAsync(order, nameof(IOrderDto.DateOfReceipt), model.DateOfReceipt);
_notificationService.SuccessNotification("Custom attributes saved successfully.");
return RedirectToAction("Edit", "Order", new { id = model.OrderId });
}
}
}

View File

@ -0,0 +1,3863 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Directory;
using Nop.Core.Domain.Discounts;
using Nop.Core.Domain.Media;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Tax;
using Nop.Core.Domain.Vendors;
using Nop.Core.Http;
using Nop.Core.Infrastructure;
using Nop.Services.Catalog;
using Nop.Services.Common;
using Nop.Services.Configuration;
using Nop.Services.Customers;
using Nop.Services.Directory;
using Nop.Services.Discounts;
using Nop.Services.ExportImport;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Media;
using Nop.Services.Messages;
using Nop.Services.Orders;
using Nop.Services.Security;
using Nop.Services.Seo;
using Nop.Services.Shipping;
using Nop.Web.Areas.Admin.Factories;
using Nop.Web.Areas.Admin.Infrastructure.Mapper.Extensions;
using Nop.Web.Areas.Admin.Models.Catalog;
using Nop.Web.Framework.Controllers;
using Nop.Web.Framework.Mvc;
using Nop.Web.Framework.Mvc.Filters;
using Nop.Web.Framework.Mvc.ModelBinding;
using Nop.Web.Framework.Validators;
using System.Text;
namespace Nop.Web.Areas.Admin.Controllers;
public partial class CustomProductController : BaseAdminController
{
#region Fields
protected readonly AdminAreaSettings _adminAreaSettings;
protected readonly IAclService _aclService;
protected readonly IBackInStockSubscriptionService _backInStockSubscriptionService;
protected readonly ICategoryService _categoryService;
protected readonly ICopyProductService _copyProductService;
protected readonly ICurrencyService _currencyService;
protected readonly ICustomerActivityService _customerActivityService;
protected readonly ICustomerService _customerService;
protected readonly IDiscountService _discountService;
protected readonly IDownloadService _downloadService;
protected readonly IExportManager _exportManager;
protected readonly IGenericAttributeService _genericAttributeService;
protected readonly IHttpClientFactory _httpClientFactory;
protected readonly IImportManager _importManager;
protected readonly ILanguageService _languageService;
protected readonly ILocalizationService _localizationService;
protected readonly ILocalizedEntityService _localizedEntityService;
protected readonly IManufacturerService _manufacturerService;
protected readonly INopFileProvider _fileProvider;
protected readonly INotificationService _notificationService;
protected readonly IPdfService _pdfService;
protected readonly IPermissionService _permissionService;
protected readonly IPictureService _pictureService;
protected readonly IProductAttributeFormatter _productAttributeFormatter;
protected readonly IProductAttributeParser _productAttributeParser;
protected readonly IProductAttributeService _productAttributeService;
protected readonly IProductModelFactory _productModelFactory;
protected readonly IProductService _productService;
protected readonly IProductTagService _productTagService;
protected readonly ISettingService _settingService;
protected readonly IShippingService _shippingService;
protected readonly IShoppingCartService _shoppingCartService;
protected readonly ISpecificationAttributeService _specificationAttributeService;
protected readonly IStoreContext _storeContext;
protected readonly IUrlRecordService _urlRecordService;
protected readonly IVideoService _videoService;
protected readonly IWebHelper _webHelper;
protected readonly IWorkContext _workContext;
protected readonly CurrencySettings _currencySettings;
protected readonly TaxSettings _taxSettings;
protected readonly VendorSettings _vendorSettings;
private static readonly char[] _separator = [','];
#endregion
#region Ctor
public CustomProductController(AdminAreaSettings adminAreaSettings,
IAclService aclService,
IBackInStockSubscriptionService backInStockSubscriptionService,
ICategoryService categoryService,
ICopyProductService copyProductService,
ICurrencyService currencyService,
ICustomerActivityService customerActivityService,
ICustomerService customerService,
IDiscountService discountService,
IDownloadService downloadService,
IExportManager exportManager,
IGenericAttributeService genericAttributeService,
IHttpClientFactory httpClientFactory,
IImportManager importManager,
ILanguageService languageService,
ILocalizationService localizationService,
ILocalizedEntityService localizedEntityService,
IManufacturerService manufacturerService,
INopFileProvider fileProvider,
INotificationService notificationService,
IPdfService pdfService,
IPermissionService permissionService,
IPictureService pictureService,
IProductAttributeFormatter productAttributeFormatter,
IProductAttributeParser productAttributeParser,
IProductAttributeService productAttributeService,
IProductModelFactory productModelFactory,
IProductService productService,
IProductTagService productTagService,
ISettingService settingService,
IShippingService shippingService,
IShoppingCartService shoppingCartService,
ISpecificationAttributeService specificationAttributeService,
IStoreContext storeContext,
IUrlRecordService urlRecordService,
IVideoService videoService,
IWebHelper webHelper,
IWorkContext workContext,
CurrencySettings currencySettings,
TaxSettings taxSettings,
VendorSettings vendorSettings)
{
_adminAreaSettings = adminAreaSettings;
_aclService = aclService;
_backInStockSubscriptionService = backInStockSubscriptionService;
_categoryService = categoryService;
_copyProductService = copyProductService;
_currencyService = currencyService;
_customerActivityService = customerActivityService;
_customerService = customerService;
_discountService = discountService;
_downloadService = downloadService;
_exportManager = exportManager;
_genericAttributeService = genericAttributeService;
_httpClientFactory = httpClientFactory;
_importManager = importManager;
_languageService = languageService;
_localizationService = localizationService;
_localizedEntityService = localizedEntityService;
_manufacturerService = manufacturerService;
_fileProvider = fileProvider;
_notificationService = notificationService;
_pdfService = pdfService;
_permissionService = permissionService;
_pictureService = pictureService;
_productAttributeFormatter = productAttributeFormatter;
_productAttributeParser = productAttributeParser;
_productAttributeService = productAttributeService;
_productModelFactory = productModelFactory;
_productService = productService;
_productTagService = productTagService;
_settingService = settingService;
_shippingService = shippingService;
_shoppingCartService = shoppingCartService;
_specificationAttributeService = specificationAttributeService;
_storeContext = storeContext;
_urlRecordService = urlRecordService;
_videoService = videoService;
_webHelper = webHelper;
_workContext = workContext;
_currencySettings = currencySettings;
_taxSettings = taxSettings;
_vendorSettings = vendorSettings;
}
#endregion
#region Utilities
protected virtual async Task UpdateLocalesAsync(Product product, ProductModel model)
{
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.Name,
localized.Name,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.ShortDescription,
localized.ShortDescription,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.FullDescription,
localized.FullDescription,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.MetaKeywords,
localized.MetaKeywords,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.MetaDescription,
localized.MetaDescription,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(product,
x => x.MetaTitle,
localized.MetaTitle,
localized.LanguageId);
//search engine name
var seName = await _urlRecordService.ValidateSeNameAsync(product, localized.SeName, localized.Name, false);
await _urlRecordService.SaveSlugAsync(product, seName, localized.LanguageId);
}
}
protected virtual async Task UpdateLocalesAsync(ProductTag productTag, ProductTagModel model)
{
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(productTag,
x => x.Name,
localized.Name,
localized.LanguageId);
var seName = await _urlRecordService.ValidateSeNameAsync(productTag, string.Empty, localized.Name, false);
await _urlRecordService.SaveSlugAsync(productTag, seName, localized.LanguageId);
}
}
protected virtual async Task UpdateLocalesAsync(ProductAttributeMapping pam, ProductAttributeMappingModel model)
{
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(pam,
x => x.TextPrompt,
localized.TextPrompt,
localized.LanguageId);
await _localizedEntityService.SaveLocalizedValueAsync(pam,
x => x.DefaultValue,
localized.DefaultValue,
localized.LanguageId);
}
}
protected virtual async Task UpdateLocalesAsync(ProductAttributeValue pav, ProductAttributeValueModel model)
{
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(pav,
x => x.Name,
localized.Name,
localized.LanguageId);
}
}
protected virtual async Task UpdatePictureSeoNamesAsync(Product product)
{
foreach (var pp in await _productService.GetProductPicturesByProductIdAsync(product.Id))
await _pictureService.SetSeoFilenameAsync(pp.PictureId, await _pictureService.GetPictureSeNameAsync(product.Name));
}
protected virtual async Task SaveCategoryMappingsAsync(Product product, ProductModel model)
{
var existingProductCategories = await _categoryService.GetProductCategoriesByProductIdAsync(product.Id, true);
//delete categories
foreach (var existingProductCategory in existingProductCategories)
if (!model.SelectedCategoryIds.Contains(existingProductCategory.CategoryId))
await _categoryService.DeleteProductCategoryAsync(existingProductCategory);
//add categories
foreach (var categoryId in model.SelectedCategoryIds)
{
var category = await _categoryService.GetCategoryByIdAsync(categoryId);
if (category is null)
continue;
if (!await _categoryService.CanVendorAddProductsAsync(category))
continue;
if (_categoryService.FindProductCategory(existingProductCategories, product.Id, categoryId) == null)
{
//find next display order
var displayOrder = 1;
var existingCategoryMapping = await _categoryService.GetProductCategoriesByCategoryIdAsync(categoryId, showHidden: true);
if (existingCategoryMapping.Any())
displayOrder = existingCategoryMapping.Max(x => x.DisplayOrder) + 1;
await _categoryService.InsertProductCategoryAsync(new ProductCategory
{
ProductId = product.Id,
CategoryId = categoryId,
DisplayOrder = displayOrder
});
}
}
}
protected virtual async Task SaveManufacturerMappingsAsync(Product product, ProductModel model)
{
var existingProductManufacturers = await _manufacturerService.GetProductManufacturersByProductIdAsync(product.Id, true);
//delete manufacturers
foreach (var existingProductManufacturer in existingProductManufacturers)
if (!model.SelectedManufacturerIds.Contains(existingProductManufacturer.ManufacturerId))
await _manufacturerService.DeleteProductManufacturerAsync(existingProductManufacturer);
//add manufacturers
foreach (var manufacturerId in model.SelectedManufacturerIds)
{
if (_manufacturerService.FindProductManufacturer(existingProductManufacturers, product.Id, manufacturerId) == null)
{
//find next display order
var displayOrder = 1;
var existingManufacturerMapping = await _manufacturerService.GetProductManufacturersByManufacturerIdAsync(manufacturerId, showHidden: true);
if (existingManufacturerMapping.Any())
displayOrder = existingManufacturerMapping.Max(x => x.DisplayOrder) + 1;
await _manufacturerService.InsertProductManufacturerAsync(new ProductManufacturer
{
ProductId = product.Id,
ManufacturerId = manufacturerId,
DisplayOrder = displayOrder
});
}
}
}
protected virtual async Task SaveDiscountMappingsAsync(Product product, ProductModel model)
{
var allDiscounts = await _discountService.GetAllDiscountsAsync(DiscountType.AssignedToSkus, showHidden: true, isActive: null);
foreach (var discount in allDiscounts)
{
if (model.SelectedDiscountIds != null && model.SelectedDiscountIds.Contains(discount.Id))
{
//new discount
if (await _productService.GetDiscountAppliedToProductAsync(product.Id, discount.Id) is null)
await _productService.InsertDiscountProductMappingAsync(new DiscountProductMapping { EntityId = product.Id, DiscountId = discount.Id });
}
else
{
//remove discount
if (await _productService.GetDiscountAppliedToProductAsync(product.Id, discount.Id) is DiscountProductMapping discountProductMapping)
await _productService.DeleteDiscountProductMappingAsync(discountProductMapping);
}
}
await _productService.UpdateProductAsync(product);
}
protected virtual async Task<string> GetAttributesXmlForProductAttributeCombinationAsync(IFormCollection form, List<string> warnings, int productId)
{
var attributesXml = string.Empty;
//get product attribute mappings (exclude non-combinable attributes)
var attributes = (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(productId))
.Where(productAttributeMapping => !productAttributeMapping.IsNonCombinable()).ToList();
foreach (var attribute in attributes)
{
var controlId = $"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}";
StringValues ctrlAttributes;
switch (attribute.AttributeControlType)
{
case AttributeControlType.DropdownList:
case AttributeControlType.RadioList:
case AttributeControlType.ColorSquares:
case AttributeControlType.ImageSquares:
ctrlAttributes = form[controlId];
if (!string.IsNullOrEmpty(ctrlAttributes))
{
var selectedAttributeId = int.Parse(ctrlAttributes);
if (selectedAttributeId > 0)
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, selectedAttributeId.ToString());
}
break;
case AttributeControlType.Checkboxes:
var cblAttributes = form[controlId].ToString();
if (!string.IsNullOrEmpty(cblAttributes))
{
foreach (var item in cblAttributes.Split(_separator,
StringSplitOptions.RemoveEmptyEntries))
{
var selectedAttributeId = int.Parse(item);
if (selectedAttributeId > 0)
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, selectedAttributeId.ToString());
}
}
break;
case AttributeControlType.ReadonlyCheckboxes:
//load read-only (already server-side selected) values
var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(attribute.Id);
foreach (var selectedAttributeId in attributeValues
.Where(v => v.IsPreSelected)
.Select(v => v.Id)
.ToList())
{
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, selectedAttributeId.ToString());
}
break;
case AttributeControlType.TextBox:
case AttributeControlType.MultilineTextbox:
ctrlAttributes = form[controlId];
if (!string.IsNullOrEmpty(ctrlAttributes))
{
var enteredText = ctrlAttributes.ToString().Trim();
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, enteredText);
}
break;
case AttributeControlType.Datepicker:
var date = form[controlId + "_day"];
var month = form[controlId + "_month"];
var year = form[controlId + "_year"];
DateTime? selectedDate = null;
try
{
selectedDate = new DateTime(int.Parse(year), int.Parse(month), int.Parse(date));
}
catch
{
//ignore any exception
}
if (selectedDate.HasValue)
{
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, selectedDate.Value.ToString("D"));
}
break;
case AttributeControlType.FileUpload:
var requestForm = await Request.ReadFormAsync();
var httpPostedFile = requestForm.Files[controlId];
if (!string.IsNullOrEmpty(httpPostedFile?.FileName))
{
var fileSizeOk = true;
if (attribute.ValidationFileMaximumSize.HasValue)
{
//compare in bytes
var maxFileSizeBytes = attribute.ValidationFileMaximumSize.Value * 1024;
if (httpPostedFile.Length > maxFileSizeBytes)
{
warnings.Add(string.Format(
await _localizationService.GetResourceAsync("ShoppingCart.MaximumUploadedFileSize"),
attribute.ValidationFileMaximumSize.Value));
fileSizeOk = false;
}
}
if (fileSizeOk)
{
//save an uploaded file
var download = new Download
{
DownloadGuid = Guid.NewGuid(),
UseDownloadUrl = false,
DownloadUrl = string.Empty,
DownloadBinary = await _downloadService.GetDownloadBitsAsync(httpPostedFile),
ContentType = httpPostedFile.ContentType,
Filename = _fileProvider.GetFileNameWithoutExtension(httpPostedFile.FileName),
Extension = _fileProvider.GetFileExtension(httpPostedFile.FileName),
IsNew = true
};
await _downloadService.InsertDownloadAsync(download);
//save attribute
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, download.DownloadGuid.ToString());
}
}
break;
default:
break;
}
}
//validate conditional attributes (if specified)
foreach (var attribute in attributes)
{
var conditionMet = await _productAttributeParser.IsConditionMetAsync(attribute, attributesXml);
if (conditionMet.HasValue && !conditionMet.Value)
{
attributesXml = _productAttributeParser.RemoveProductAttribute(attributesXml, attribute);
}
}
return attributesXml;
}
protected virtual async Task SaveProductWarehouseInventoryAsync(Product product, ProductModel model)
{
ArgumentNullException.ThrowIfNull(product);
if (model.ManageInventoryMethodId != (int)ManageInventoryMethod.ManageStock)
return;
if (!model.UseMultipleWarehouses)
return;
var warehouses = await _shippingService.GetAllWarehousesAsync();
var form = await Request.ReadFormAsync();
var formData = form.ToDictionary(x => x.Key, x => x.Value.ToString());
foreach (var warehouse in warehouses)
{
//parse stock quantity
var stockQuantity = 0;
foreach (var formKey in formData.Keys)
{
if (!formKey.Equals($"warehouse_qty_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase))
continue;
_ = int.TryParse(formData[formKey], out stockQuantity);
break;
}
//parse reserved quantity
var reservedQuantity = 0;
foreach (var formKey in formData.Keys)
if (formKey.Equals($"warehouse_reserved_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase))
{
_ = int.TryParse(formData[formKey], out reservedQuantity);
break;
}
//parse "used" field
var used = false;
foreach (var formKey in formData.Keys)
if (formKey.Equals($"warehouse_used_{warehouse.Id}", StringComparison.InvariantCultureIgnoreCase))
{
_ = int.TryParse(formData[formKey], out var tmp);
used = tmp == warehouse.Id;
break;
}
//quantity change history message
var message = $"{await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.MultipleWarehouses")} {await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit")}";
var existingPwI = (await _productService.GetAllProductWarehouseInventoryRecordsAsync(product.Id)).FirstOrDefault(x => x.WarehouseId == warehouse.Id);
if (existingPwI != null)
{
if (used)
{
var previousStockQuantity = existingPwI.StockQuantity;
//update existing record
existingPwI.StockQuantity = stockQuantity;
existingPwI.ReservedQuantity = reservedQuantity;
await _productService.UpdateProductWarehouseInventoryAsync(existingPwI);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, existingPwI.StockQuantity - previousStockQuantity, existingPwI.StockQuantity,
existingPwI.WarehouseId, message);
}
else
{
//delete. no need to store record for qty 0
await _productService.DeleteProductWarehouseInventoryAsync(existingPwI);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, -existingPwI.StockQuantity, 0, existingPwI.WarehouseId, message);
}
}
else
{
if (!used)
continue;
//no need to insert a record for qty 0
existingPwI = new ProductWarehouseInventory
{
WarehouseId = warehouse.Id,
ProductId = product.Id,
StockQuantity = stockQuantity,
ReservedQuantity = reservedQuantity
};
await _productService.InsertProductWarehouseInventoryAsync(existingPwI);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, existingPwI.StockQuantity, existingPwI.StockQuantity,
existingPwI.WarehouseId, message);
}
}
}
protected virtual async Task SaveConditionAttributesAsync(ProductAttributeMapping productAttributeMapping,
ProductAttributeConditionModel model, IFormCollection form)
{
string attributesXml = null;
if (model.EnableCondition)
{
var attribute = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.SelectedProductAttributeId);
if (attribute != null)
{
var controlId = $"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}";
switch (attribute.AttributeControlType)
{
case AttributeControlType.DropdownList:
case AttributeControlType.RadioList:
case AttributeControlType.ColorSquares:
case AttributeControlType.ImageSquares:
var ctrlAttributes = form[controlId];
if (!StringValues.IsNullOrEmpty(ctrlAttributes))
{
var selectedAttributeId = int.Parse(ctrlAttributes);
//for conditions we should empty values save even when nothing is selected
//otherwise "attributesXml" will be empty
//hence we won't be able to find a selected attribute
attributesXml = _productAttributeParser.AddProductAttribute(null, attribute,
selectedAttributeId > 0 ? selectedAttributeId.ToString() : string.Empty);
}
else
{
//for conditions we should empty values save even when nothing is selected
//otherwise "attributesXml" will be empty
//hence we won't be able to find a selected attribute
attributesXml = _productAttributeParser.AddProductAttribute(null,
attribute, string.Empty);
}
break;
case AttributeControlType.Checkboxes:
var cblAttributes = form[controlId];
if (!StringValues.IsNullOrEmpty(cblAttributes))
{
var anyValueSelected = false;
foreach (var item in cblAttributes.ToString()
.Split(_separator, StringSplitOptions.RemoveEmptyEntries))
{
var selectedAttributeId = int.Parse(item);
if (selectedAttributeId <= 0)
continue;
attributesXml = _productAttributeParser.AddProductAttribute(attributesXml,
attribute, selectedAttributeId.ToString());
anyValueSelected = true;
}
if (!anyValueSelected)
{
//for conditions we should save empty values even when nothing is selected
//otherwise "attributesXml" will be empty
//hence we won't be able to find a selected attribute
attributesXml = _productAttributeParser.AddProductAttribute(null,
attribute, string.Empty);
}
}
else
{
//for conditions we should save empty values even when nothing is selected
//otherwise "attributesXml" will be empty
//hence we won't be able to find a selected attribute
attributesXml = _productAttributeParser.AddProductAttribute(null,
attribute, string.Empty);
}
break;
case AttributeControlType.ReadonlyCheckboxes:
case AttributeControlType.TextBox:
case AttributeControlType.MultilineTextbox:
case AttributeControlType.Datepicker:
case AttributeControlType.FileUpload:
default:
//these attribute types are supported as conditions
break;
}
}
}
productAttributeMapping.ConditionAttributeXml = attributesXml;
await _productAttributeService.UpdateProductAttributeMappingAsync(productAttributeMapping);
}
protected virtual async Task GenerateAttributeCombinationsAsync(Product product, IList<int> allowedAttributeIds = null)
{
var allAttributesXml = await _productAttributeParser.GenerateAllCombinationsAsync(product, true, allowedAttributeIds);
foreach (var attributesXml in allAttributesXml)
{
var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
//already exists?
if (existingCombination != null)
continue;
//new one
var warnings = new List<string>();
warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(),
ShoppingCartType.ShoppingCart, product, 1, attributesXml, true, true, true));
if (warnings.Any())
continue;
//save combination
var combination = new ProductAttributeCombination
{
ProductId = product.Id,
AttributesXml = attributesXml,
StockQuantity = 0,
AllowOutOfStockOrders = false,
Sku = null,
ManufacturerPartNumber = null,
Gtin = null,
OverriddenPrice = null,
NotifyAdminForQuantityBelow = 1
};
await _productAttributeService.InsertProductAttributeCombinationAsync(combination);
}
}
protected virtual async Task PingVideoUrlAsync(string videoUrl)
{
var path = videoUrl.StartsWith('/')
? $"{_webHelper.GetStoreLocation()}{videoUrl.TrimStart('/')}"
: videoUrl;
var client = _httpClientFactory.CreateClient(NopHttpDefaults.DefaultHttpClient);
await client.GetStringAsync(path);
}
protected virtual async Task SaveAttributeCombinationPicturesAsync(Product product, ProductAttributeCombination combination, ProductAttributeCombinationModel model)
{
var existingCombinationPictures = await _productAttributeService.GetProductAttributeCombinationPicturesAsync(combination.Id);
var productPictureIds = (await _pictureService.GetPicturesByProductIdAsync(product.Id)).Select(p => p.Id).ToList();
//delete manufacturers
foreach (var existingCombinationPicture in existingCombinationPictures)
if (!model.PictureIds.Contains(existingCombinationPicture.PictureId) || !productPictureIds.Contains(existingCombinationPicture.PictureId))
await _productAttributeService.DeleteProductAttributeCombinationPictureAsync(existingCombinationPicture);
//add manufacturers
foreach (var pictureId in model.PictureIds)
{
if (!productPictureIds.Contains(pictureId))
continue;
if (_productAttributeService.FindProductAttributeCombinationPicture(existingCombinationPictures, combination.Id, pictureId) == null)
{
await _productAttributeService.InsertProductAttributeCombinationPictureAsync(new ProductAttributeCombinationPicture
{
ProductAttributeCombinationId = combination.Id,
PictureId = pictureId
});
}
}
}
protected virtual async Task SaveAttributeValuePicturesAsync(Product product, ProductAttributeValue value, ProductAttributeValueModel model)
{
var existingValuePictures = await _productAttributeService.GetProductAttributeValuePicturesAsync(value.Id);
var productPictureIds = (await _pictureService.GetPicturesByProductIdAsync(product.Id)).Select(p => p.Id).ToList();
//delete manufacturers
foreach (var existingValuePicture in existingValuePictures)
if (!model.PictureIds.Contains(existingValuePicture.PictureId) || !productPictureIds.Contains(existingValuePicture.PictureId))
await _productAttributeService.DeleteProductAttributeValuePictureAsync(existingValuePicture);
//add manufacturers
foreach (var pictureId in model.PictureIds)
{
if (!productPictureIds.Contains(pictureId))
continue;
if (_productAttributeService.FindProductAttributeValuePicture(existingValuePictures, value.Id, pictureId) == null)
{
await _productAttributeService.InsertProductAttributeValuePictureAsync(new ProductAttributeValuePicture
{
ProductAttributeValueId = value.Id,
PictureId = pictureId
});
}
}
}
protected virtual async Task<List<BulkEditData>> ParseBulkEditDataAsync()
{
var rez = new Dictionary<int, BulkEditData>();
var currentVendor = await _workContext.GetCurrentVendorAsync();
foreach (var item in Request.Form)
{
if (getData(item, "product-select-", out var productId))
setData(productId, data =>
{
data.IsSelected = true;
});
if (getData(item, "name-", out productId))
setData(productId, data =>
{
data.Name = item.Value;
});
if (getData(item, "sku-", out productId))
setData(productId, data =>
{
data.Sku = item.Value;
});
if (getData(item, "price-", out productId))
setData(productId, data =>
{
data.Price = decimal.Parse(item.Value);
});
if (getData(item, "old-price-", out productId))
setData(productId, data =>
{
data.OldPrice = decimal.Parse(item.Value);
});
if (getData(item, "quantity-", out productId))
setData(productId, data =>
{
data.Quantity = int.Parse(item.Value);
});
if (getData(item, "published-", out productId))
setData(productId, data =>
{
data.IsPublished = true;
});
}
var productIds = rez.Select(p => p.Key).ToArray();
var products = await _productService.GetProductsByIdsAsync(productIds);
foreach (var product in products)
rez[product.Id].Product = product;
return rez.Values.ToList();
bool getData(KeyValuePair<string, StringValues> item, string selector, out int productId)
{
var key = item.Key;
productId = 0;
if (!key.StartsWith(selector))
return false;
productId = int.Parse(key.Replace(selector, string.Empty));
return true;
}
void setData(int productId, Action<BulkEditData> action)
{
if (!rez.ContainsKey(productId))
rez.Add(productId, new BulkEditData(_taxSettings.DefaultTaxCategoryId, currentVendor?.Id ?? 0));
action(rez[productId]);
}
}
#endregion
#region Methods
#region Product list / create / edit / delete
public virtual IActionResult Index()
{
return RedirectToAction("List");
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> List()
{
//prepare model
var model = await _productModelFactory.PrepareProductSearchModelAsync(new ProductSearchModel());
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Product/List.cshtml", model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> BulkEdit()
{
//prepare model
var model = await _productModelFactory.PrepareProductSearchModelAsync(new ProductSearchModel());
model.Length = _adminAreaSettings.ProductsBulkEditGridPageSize;
return View(model);
}
[HttpPost, ActionName("BulkEdit"), ParameterBasedOnFormName("bulk-edit-save-selected", "selected")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public async Task<IActionResult> BulkEditSave(ProductSearchModel searchModel, bool selected)
{
var data = await ParseBulkEditDataAsync();
var productsToUpdate = data.Where(d => d.NeedToUpdate(selected)).ToList();
await _productService.UpdateProductsAsync(productsToUpdate.Select(d => d.UpdateProduct(selected)).ToList());
var productsToInsert = data.Where(d => d.NeedToCreate(selected)).ToList();
await _productService.InsertProductsAsync(productsToInsert.Select(d => d.CreateProduct(selected)).ToList());
//prepare model
var model = await _productModelFactory.PrepareProductSearchModelAsync(searchModel);
model.Length = _adminAreaSettings.ProductsBulkEditGridPageSize;
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductList(ProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareProductListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> BulkEditProducts(ProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareProductListModelAsync(searchModel);
var html = await RenderPartialViewToStringAsync("_BulkEdit.Products", model.Data.ToList());
return Json(new Dictionary<string, object> { { "Html", html }, { "Products", model } });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> BulkEditNewProduct(int id)
{
var primaryStoreCurrencyCode = (await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId)).CurrencyCode;
//prepare model
var model = new List<ProductModel> { new()
{
Id = id,
PrimaryStoreCurrencyCode = primaryStoreCurrencyCode,
Published = true
} };
var html = await RenderPartialViewToStringAsync("_BulkEdit.Products", model);
return Json(html);
}
[HttpPost, ActionName("List")]
[FormValueRequired("go-to-product-by-sku")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> GoToSku(ProductSearchModel searchModel)
{
//try to load a product entity, if not found, then try to load a product attribute combination
var productId = (await _productService.GetProductBySkuAsync(searchModel.GoDirectlyToSku))?.Id
?? (await _productAttributeService.GetProductAttributeCombinationBySkuAsync(searchModel.GoDirectlyToSku))?.ProductId;
if (productId != null)
return RedirectToAction("Edit", "Product", new { id = productId });
//not found
return await List();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Create(bool showtour = false)
{
//validate maximum number of products per vendor
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (_vendorSettings.MaximumProductNumber > 0 &&
currentVendor != null &&
await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) >= _vendorSettings.MaximumProductNumber)
{
_notificationService.ErrorNotification(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ExceededMaximumNumber"),
_vendorSettings.MaximumProductNumber));
return RedirectToAction("List");
}
//prepare model
var model = await _productModelFactory.PrepareProductModelAsync(new ProductModel(), null);
//show configuration tour
if (showtour)
{
var customer = await _workContext.GetCurrentCustomerAsync();
var hideCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.HideConfigurationStepsAttribute);
var closeCard = await _genericAttributeService.GetAttributeAsync<bool>(customer, NopCustomerDefaults.CloseConfigurationStepsAttribute);
if (!hideCard && !closeCard)
ViewBag.ShowTour = true;
}
return View(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Create(ProductModel model, bool continueEditing)
{
//validate maximum number of products per vendor
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (_vendorSettings.MaximumProductNumber > 0 &&
currentVendor != null &&
await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) >= _vendorSettings.MaximumProductNumber)
{
_notificationService.ErrorNotification(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ExceededMaximumNumber"),
_vendorSettings.MaximumProductNumber));
return RedirectToAction("List");
}
if (ModelState.IsValid)
{
//a vendor should have access only to his products
if (currentVendor != null)
model.VendorId = currentVendor.Id;
//vendors cannot edit "Show on home page" property
if (currentVendor != null && model.ShowOnHomepage)
model.ShowOnHomepage = false;
//product
var product = model.ToEntity<Product>();
product.CreatedOnUtc = DateTime.UtcNow;
product.UpdatedOnUtc = DateTime.UtcNow;
await _productService.InsertProductAsync(product);
//search engine name
model.SeName = await _urlRecordService.ValidateSeNameAsync(product, model.SeName, product.Name, true);
await _urlRecordService.SaveSlugAsync(product, model.SeName, 0);
//locales
await UpdateLocalesAsync(product, model);
//categories
await SaveCategoryMappingsAsync(product, model);
//manufacturers
await SaveManufacturerMappingsAsync(product, model);
//stores
await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds);
//discounts
await SaveDiscountMappingsAsync(product, model);
//tags
await _productTagService.UpdateProductTagsAsync(product, model.SelectedProductTags.ToArray());
//warehouses
await SaveProductWarehouseInventoryAsync(product, model);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity, product.StockQuantity, product.WarehouseId,
await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit"));
//activity log
await _customerActivityService.InsertActivityAsync("AddNewProduct",
string.Format(await _localizationService.GetResourceAsync("ActivityLog.AddNewProduct"), product.Name), product);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Added"));
if (!continueEditing)
return RedirectToAction("List");
return RedirectToAction("Edit", new { id = product.Id });
}
//prepare model
model = await _productModelFactory.PrepareProductModelAsync(model, null, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> Edit(int id)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(id);
if (product == null || product.Deleted)
return RedirectToAction("List");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List");
//prepare model
var model = await _productModelFactory.PrepareProductModelAsync(null, product);
return View(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Edit(ProductModel model, bool continueEditing)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(model.Id);
if (product == null || product.Deleted)
return RedirectToAction("List");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List");
//check if the product quantity has been changed while we were editing the product
//and if it has been changed then we show error notification
//and redirect on the editing page without data saving
if (product.StockQuantity != model.LastStockQuantity)
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.StockQuantity.ChangedWarning"));
return RedirectToAction("Edit", new { id = product.Id });
}
if (ModelState.IsValid)
{
//a vendor should have access only to his products
if (currentVendor != null)
model.VendorId = currentVendor.Id;
//we do not validate maximum number of products per vendor when editing existing products (only during creation of new products)
//vendors cannot edit "Show on home page" property
if (currentVendor != null && model.ShowOnHomepage != product.ShowOnHomepage)
model.ShowOnHomepage = product.ShowOnHomepage;
//some previously used values
var prevTotalStockQuantity = await _productService.GetTotalStockQuantityAsync(product);
var prevDownloadId = product.DownloadId;
var prevSampleDownloadId = product.SampleDownloadId;
var previousStockQuantity = product.StockQuantity;
var previousWarehouseId = product.WarehouseId;
var previousProductType = product.ProductType;
//product
product = model.ToEntity(product);
product.UpdatedOnUtc = DateTime.UtcNow;
await _productService.UpdateProductAsync(product);
//remove associated products
if (previousProductType == ProductType.GroupedProduct && product.ProductType == ProductType.SimpleProduct)
{
var store = await _storeContext.GetCurrentStoreAsync();
var storeId = store?.Id ?? 0;
var vendorId = currentVendor?.Id ?? 0;
var associatedProducts = await _productService.GetAssociatedProductsAsync(product.Id, storeId, vendorId);
foreach (var associatedProduct in associatedProducts)
{
associatedProduct.ParentGroupedProductId = 0;
await _productService.UpdateProductAsync(associatedProduct);
}
}
//search engine name
model.SeName = await _urlRecordService.ValidateSeNameAsync(product, model.SeName, product.Name, true);
await _urlRecordService.SaveSlugAsync(product, model.SeName, 0);
//locales
await UpdateLocalesAsync(product, model);
//tags
await _productTagService.UpdateProductTagsAsync(product, model.SelectedProductTags.ToArray());
//warehouses
await SaveProductWarehouseInventoryAsync(product, model);
//categories
await SaveCategoryMappingsAsync(product, model);
//manufacturers
await SaveManufacturerMappingsAsync(product, model);
//stores
await _productService.UpdateProductStoreMappingsAsync(product, model.SelectedStoreIds);
//discounts
await SaveDiscountMappingsAsync(product, model);
//picture seo names
await UpdatePictureSeoNamesAsync(product);
//back in stock notifications
if (product.ManageInventoryMethod == ManageInventoryMethod.ManageStock &&
product.BackorderMode == BackorderMode.NoBackorders &&
product.AllowBackInStockSubscriptions &&
await _productService.GetTotalStockQuantityAsync(product) > 0 &&
prevTotalStockQuantity <= 0 &&
product.Published &&
!product.Deleted)
{
await _backInStockSubscriptionService.SendNotificationsToSubscribersAsync(product);
}
//delete an old "download" file (if deleted or updated)
if (prevDownloadId > 0 && prevDownloadId != product.DownloadId)
{
var prevDownload = await _downloadService.GetDownloadByIdAsync(prevDownloadId);
if (prevDownload != null)
await _downloadService.DeleteDownloadAsync(prevDownload);
}
//delete an old "sample download" file (if deleted or updated)
if (prevSampleDownloadId > 0 && prevSampleDownloadId != product.SampleDownloadId)
{
var prevSampleDownload = await _downloadService.GetDownloadByIdAsync(prevSampleDownloadId);
if (prevSampleDownload != null)
await _downloadService.DeleteDownloadAsync(prevSampleDownload);
}
//quantity change history
if (previousWarehouseId != product.WarehouseId)
{
//warehouse is changed
//compose a message
var oldWarehouseMessage = string.Empty;
if (previousWarehouseId > 0)
{
var oldWarehouse = await _shippingService.GetWarehouseByIdAsync(previousWarehouseId);
if (oldWarehouse != null)
oldWarehouseMessage = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse.Old"), oldWarehouse.Name);
}
var newWarehouseMessage = string.Empty;
if (product.WarehouseId > 0)
{
var newWarehouse = await _shippingService.GetWarehouseByIdAsync(product.WarehouseId);
if (newWarehouse != null)
newWarehouseMessage = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse.New"), newWarehouse.Name);
}
var message = string.Format(await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.EditWarehouse"), oldWarehouseMessage, newWarehouseMessage);
//record history
await _productService.AddStockQuantityHistoryEntryAsync(product, -previousStockQuantity, 0, previousWarehouseId, message);
await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity, product.StockQuantity, product.WarehouseId, message);
}
else
{
await _productService.AddStockQuantityHistoryEntryAsync(product, product.StockQuantity - previousStockQuantity, product.StockQuantity,
product.WarehouseId, await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Edit"));
}
//activity log
await _customerActivityService.InsertActivityAsync("EditProduct",
string.Format(await _localizationService.GetResourceAsync("ActivityLog.EditProduct"), product.Name), product);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Updated"));
if (!continueEditing)
return RedirectToAction("List");
return RedirectToAction("Edit", new { id = product.Id });
}
//prepare model
model = await _productModelFactory.PrepareProductModelAsync(model, product, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> Delete(int id)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
return RedirectToAction("List");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List");
await _productService.DeleteProductAsync(product);
//activity log
await _customerActivityService.InsertActivityAsync("DeleteProduct",
string.Format(await _localizationService.GetResourceAsync("ActivityLog.DeleteProduct"), product.Name), product);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Deleted"));
return RedirectToAction("List");
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> DeleteSelected(ICollection<int> selectedIds)
{
if (selectedIds == null || !selectedIds.Any())
return NoContent();
var currentVendor = await _workContext.GetCurrentVendorAsync();
var products = (await _productService.GetProductsByIdsAsync(selectedIds.ToArray()))
.Where(p => currentVendor == null || p.VendorId == currentVendor.Id).ToList();
await _productService.DeleteProductsAsync(products);
//activity log
var activityLogFormat = await _localizationService.GetResourceAsync("ActivityLog.DeleteProduct");
foreach (var product in products)
await _customerActivityService.InsertActivityAsync("DeleteProduct",
string.Format(activityLogFormat, product.Name), product);
return Json(new { Result = true });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> CopyProduct(ProductModel model)
{
var copyModel = model.CopyProductModel;
try
{
var originalProduct = await _productService.GetProductByIdAsync(copyModel.Id);
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && originalProduct.VendorId != currentVendor.Id)
return RedirectToAction("List");
var newProduct = await _copyProductService.CopyProductAsync(originalProduct, copyModel.Name, copyModel.Published, copyModel.CopyMultimedia);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Copied"));
return RedirectToAction("Edit", new { id = newProduct.Id });
}
catch (Exception exc)
{
_notificationService.ErrorNotification(exc.Message);
return RedirectToAction("Edit", new { id = copyModel.Id });
}
}
//action displaying notification (warning) to a store owner that entered SKU already exists
public virtual async Task<IActionResult> SkuReservedWarning(int productId, string sku)
{
string message;
//check whether product with passed SKU already exists
var productBySku = await _productService.GetProductBySkuAsync(sku);
if (productBySku != null)
{
if (productBySku.Id == productId)
return Json(new { Result = string.Empty });
message = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Fields.Sku.Reserved"), productBySku.Name);
return Json(new { Result = message });
}
//check whether combination with passed SKU already exists
var combinationBySku = await _productAttributeService.GetProductAttributeCombinationBySkuAsync(sku);
if (combinationBySku == null)
return Json(new { Result = string.Empty });
message = string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Fields.Sku.Reserved"),
(await _productService.GetProductByIdAsync(combinationBySku.ProductId))?.Name);
return Json(new { Result = message });
}
#endregion
#region Required products
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> LoadProductFriendlyNames(string productIds)
{
var result = string.Empty;
if (string.IsNullOrWhiteSpace(productIds))
return Json(new { Text = result });
var ids = new List<int>();
var rangeArray = productIds
.Split(_separator, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToList();
foreach (var str1 in rangeArray)
{
if (int.TryParse(str1, out var tmp1))
ids.Add(tmp1);
}
var products = await _productService.GetProductsByIdsAsync(ids.ToArray());
for (var i = 0; i <= products.Count - 1; i++)
{
result += products[i].Name;
if (i != products.Count - 1)
result += ", ";
}
return Json(new { Text = result });
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RequiredProductAddPopup()
{
//prepare model
var model = await _productModelFactory.PrepareAddRequiredProductSearchModelAsync(new AddRequiredProductSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RequiredProductAddPopupList(AddRequiredProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareAddRequiredProductListModelAsync(searchModel);
return Json(model);
}
#endregion
#region Related products
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> RelatedProductList(RelatedProductSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareRelatedProductListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RelatedProductUpdate(RelatedProductModel model)
{
//try to get a related product with the specified id
var relatedProduct = await _productService.GetRelatedProductByIdAsync(model.Id)
?? throw new ArgumentException("No related product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(relatedProduct.ProductId1);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
relatedProduct.DisplayOrder = model.DisplayOrder;
await _productService.UpdateRelatedProductAsync(relatedProduct);
return new NullJsonResult();
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RelatedProductDelete(int id)
{
//try to get a related product with the specified id
var relatedProduct = await _productService.GetRelatedProductByIdAsync(id)
?? throw new ArgumentException("No related product found with the specified id");
var productId = relatedProduct.ProductId1;
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(productId);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
await _productService.DeleteRelatedProductAsync(relatedProduct);
return new NullJsonResult();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RelatedProductAddPopup(int productId)
{
//prepare model
var model = await _productModelFactory.PrepareAddRelatedProductSearchModelAsync(new AddRelatedProductSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RelatedProductAddPopupList(AddRelatedProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareAddRelatedProductListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[FormValueRequired("save")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> RelatedProductAddPopup(AddRelatedProductModel model)
{
var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray());
if (selectedProducts.Any())
{
var existingRelatedProducts = await _productService.GetRelatedProductsByProductId1Async(model.ProductId, showHidden: true);
var currentVendor = await _workContext.GetCurrentVendorAsync();
foreach (var product in selectedProducts)
{
//a vendor should have access only to his products
if (currentVendor != null && product.VendorId != currentVendor.Id)
continue;
if (_productService.FindRelatedProduct(existingRelatedProducts, model.ProductId, product.Id) != null)
continue;
await _productService.InsertRelatedProductAsync(new RelatedProduct
{
ProductId1 = model.ProductId,
ProductId2 = product.Id,
DisplayOrder = 1
});
}
}
ViewBag.RefreshPage = true;
return View(new AddRelatedProductSearchModel());
}
#endregion
#region Cross-sell products
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> CrossSellProductList(CrossSellProductSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareCrossSellProductListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> CrossSellProductDelete(int id)
{
//try to get a cross-sell product with the specified id
var crossSellProduct = await _productService.GetCrossSellProductByIdAsync(id)
?? throw new ArgumentException("No cross-sell product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(crossSellProduct.ProductId1);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
await _productService.DeleteCrossSellProductAsync(crossSellProduct);
return new NullJsonResult();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> CrossSellProductAddPopup(int productId)
{
//prepare model
var model = await _productModelFactory.PrepareAddCrossSellProductSearchModelAsync(new AddCrossSellProductSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> CrossSellProductAddPopupList(AddCrossSellProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareAddCrossSellProductListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[FormValueRequired("save")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> CrossSellProductAddPopup(AddCrossSellProductModel model)
{
var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray());
if (selectedProducts.Any())
{
var existingCrossSellProducts = await _productService.GetCrossSellProductsByProductId1Async(model.ProductId, showHidden: true);
var currentVendor = await _workContext.GetCurrentVendorAsync();
foreach (var product in selectedProducts)
{
//a vendor should have access only to his products
if (currentVendor != null && product.VendorId != currentVendor.Id)
continue;
if (_productService.FindCrossSellProduct(existingCrossSellProducts, model.ProductId, product.Id) != null)
continue;
await _productService.InsertCrossSellProductAsync(new CrossSellProduct
{
ProductId1 = model.ProductId,
ProductId2 = product.Id
});
}
}
ViewBag.RefreshPage = true;
return View(new AddCrossSellProductSearchModel());
}
#endregion
#region Associated products
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> AssociatedProductList(AssociatedProductSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareAssociatedProductListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociatedProductUpdate(AssociatedProductModel model)
{
//try to get an associated product with the specified id
var associatedProduct = await _productService.GetProductByIdAsync(model.Id)
?? throw new ArgumentException("No associated product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && associatedProduct.VendorId != currentVendor.Id)
return Content("This is not your product");
associatedProduct.DisplayOrder = model.DisplayOrder;
await _productService.UpdateProductAsync(associatedProduct);
return new NullJsonResult();
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociatedProductDelete(int id)
{
//try to get an associated product with the specified id
var product = await _productService.GetProductByIdAsync(id)
?? throw new ArgumentException("No associated product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
product.ParentGroupedProductId = 0;
await _productService.UpdateProductAsync(product);
return new NullJsonResult();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociatedProductAddPopup(int productId)
{
//prepare model
var model = await _productModelFactory.PrepareAddAssociatedProductSearchModelAsync(new AddAssociatedProductSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociatedProductAddPopupList(AddAssociatedProductSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareAddAssociatedProductListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[FormValueRequired("save")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociatedProductAddPopup(AddAssociatedProductModel model)
{
var selectedProducts = await _productService.GetProductsByIdsAsync(model.SelectedProductIds.ToArray());
var tryToAddSelfGroupedProduct = selectedProducts
.Select(p => p.Id)
.Contains(model.ProductId);
if (selectedProducts.Any())
{
foreach (var product in selectedProducts)
{
if (product.Id == model.ProductId)
continue;
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
continue;
product.ParentGroupedProductId = model.ProductId;
await _productService.UpdateProductAsync(product);
}
}
if (tryToAddSelfGroupedProduct)
{
_notificationService.WarningNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.AssociatedProducts.TryToAddSelfGroupedProduct"));
var addAssociatedProductSearchModel = await _productModelFactory.PrepareAddAssociatedProductSearchModelAsync(new AddAssociatedProductSearchModel());
//set current product id
addAssociatedProductSearchModel.ProductId = model.ProductId;
ViewBag.RefreshPage = true;
return View(addAssociatedProductSearchModel);
}
ViewBag.RefreshPage = true;
ViewBag.ClosePage = true;
return View(new AddAssociatedProductSearchModel());
}
#endregion
#region Product pictures
[HttpPost]
[IgnoreAntiforgeryToken]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductPictureAdd(int productId, IFormCollection form)
{
if (productId == 0)
throw new ArgumentException();
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId)
?? throw new ArgumentException("No product found with the specified id");
var files = form.Files.ToList();
if (!files.Any())
return Json(new { success = false });
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List");
try
{
foreach (var file in files)
{
//insert picture
var picture = await _pictureService.InsertPictureAsync(file);
await _pictureService.SetSeoFilenameAsync(picture.Id, await _pictureService.GetPictureSeNameAsync(product.Name));
await _productService.InsertProductPictureAsync(new ProductPicture
{
PictureId = picture.Id,
ProductId = product.Id,
DisplayOrder = 0
});
}
}
catch (Exception exc)
{
return Json(new
{
success = false,
message = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Pictures.Alert.PictureAdd")} {exc.Message}",
});
}
return Json(new { success = true });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductPictureList(ProductPictureSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductPictureListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductPictureUpdate(ProductPictureModel model)
{
//try to get a product picture with the specified id
var productPicture = await _productService.GetProductPictureByIdAsync(model.Id)
?? throw new ArgumentException("No product picture found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(productPicture.ProductId);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
//try to get a picture with the specified id
var picture = await _pictureService.GetPictureByIdAsync(productPicture.PictureId)
?? throw new ArgumentException("No picture found with the specified id");
await _pictureService.UpdatePictureAsync(picture.Id,
await _pictureService.LoadPictureBinaryAsync(picture),
picture.MimeType,
picture.SeoFilename,
model.OverrideAltAttribute,
model.OverrideTitleAttribute);
productPicture.DisplayOrder = model.DisplayOrder;
await _productService.UpdateProductPictureAsync(productPicture);
return new NullJsonResult();
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductPictureDelete(int id)
{
//try to get a product picture with the specified id
var productPicture = await _productService.GetProductPictureByIdAsync(id)
?? throw new ArgumentException("No product picture found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(productPicture.ProductId);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
var pictureId = productPicture.PictureId;
await _productService.DeleteProductPictureAsync(productPicture);
//try to get a picture with the specified id
var picture = await _pictureService.GetPictureByIdAsync(pictureId)
?? throw new ArgumentException("No picture found with the specified id");
await _pictureService.DeletePictureAsync(picture);
return new NullJsonResult();
}
#endregion
#region Product videos
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductVideoAdd(int productId, [Validate] ProductVideoModel model)
{
if (productId == 0)
throw new ArgumentException();
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId)
?? throw new ArgumentException("No product found with the specified id");
if (string.IsNullOrEmpty(model.VideoUrl))
ModelState.AddModelError(string.Empty,
await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd.EmptyUrl"));
if (!ModelState.IsValid)
return ErrorJson(ModelState.SerializeErrors());
var videoUrl = model.VideoUrl.TrimStart('~');
try
{
await PingVideoUrlAsync(videoUrl);
}
catch (Exception exc)
{
return Json(new
{
success = false,
error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd")} {exc.Message}",
});
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List");
try
{
var video = new Video
{
VideoUrl = videoUrl
};
//insert video
await _videoService.InsertVideoAsync(video);
await _productService.InsertProductVideoAsync(new ProductVideo
{
VideoId = video.Id,
ProductId = product.Id,
DisplayOrder = model.DisplayOrder
});
}
catch (Exception exc)
{
return Json(new
{
success = false,
error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoAdd")} {exc.Message}",
});
}
return Json(new { success = true });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductVideoList(ProductVideoSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductVideoListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductVideoUpdate([Validate] ProductVideoModel model)
{
//try to get a product picture with the specified id
var productVideo = await _productService.GetProductVideoByIdAsync(model.Id)
?? throw new ArgumentException("No product video found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(productVideo.ProductId);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
//try to get a video with the specified id
var video = await _videoService.GetVideoByIdAsync(productVideo.VideoId)
?? throw new ArgumentException("No video found with the specified id");
var videoUrl = model.VideoUrl.TrimStart('~');
try
{
await PingVideoUrlAsync(videoUrl);
}
catch (Exception exc)
{
return Json(new
{
success = false,
error = $"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.Multimedia.Videos.Alert.VideoUpdate")} {exc.Message}",
});
}
video.VideoUrl = videoUrl;
await _videoService.UpdateVideoAsync(video);
productVideo.DisplayOrder = model.DisplayOrder;
await _productService.UpdateProductVideoAsync(productVideo);
return new NullJsonResult();
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductVideoDelete(int id)
{
//try to get a product video with the specified id
var productVideo = await _productService.GetProductVideoByIdAsync(id)
?? throw new ArgumentException("No product video found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
var product = await _productService.GetProductByIdAsync(productVideo.ProductId);
if (product != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
}
var videoId = productVideo.VideoId;
await _productService.DeleteProductVideoAsync(productVideo);
//try to get a video with the specified id
var video = await _videoService.GetVideoByIdAsync(videoId)
?? throw new ArgumentException("No video found with the specified id");
await _videoService.DeleteVideoAsync(video);
return new NullJsonResult();
}
#endregion
#region Product specification attributes
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductSpecificationAttributeAdd(AddSpecificationAttributeModel model, bool continueEditing)
{
var product = await _productService.GetProductByIdAsync(model.ProductId);
if (product == null)
{
_notificationService.ErrorNotification("No product found with the specified id");
return RedirectToAction("List");
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
{
return RedirectToAction("List");
}
//we allow filtering only for "Option" attribute type
if (model.AttributeTypeId != (int)SpecificationAttributeType.Option)
model.AllowFiltering = false;
//we don't allow CustomValue for "Option" attribute type
if (model.AttributeTypeId == (int)SpecificationAttributeType.Option)
model.ValueRaw = null;
//store raw html if field allow this
if (model.AttributeTypeId == (int)SpecificationAttributeType.CustomText || model.AttributeTypeId == (int)SpecificationAttributeType.Hyperlink)
model.ValueRaw = model.Value;
var psa = model.ToEntity<ProductSpecificationAttribute>();
psa.CustomValue = model.ValueRaw;
await _specificationAttributeService.InsertProductSpecificationAttributeAsync(psa);
switch (psa.AttributeType)
{
case SpecificationAttributeType.CustomText:
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(psa,
x => x.CustomValue,
localized.Value,
localized.LanguageId);
}
break;
case SpecificationAttributeType.CustomHtmlText:
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(psa,
x => x.CustomValue,
localized.ValueRaw,
localized.LanguageId);
}
break;
case SpecificationAttributeType.Option:
break;
case SpecificationAttributeType.Hyperlink:
break;
default:
throw new ArgumentOutOfRangeException();
}
if (continueEditing)
return RedirectToAction("ProductSpecAttributeAddOrEdit",
new { productId = psa.ProductId, specificationId = psa.Id });
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
return RedirectToAction("Edit", new { id = model.ProductId });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductSpecAttrList(ProductSpecificationAttributeSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductSpecificationAttributeListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductSpecAttrUpdate(AddSpecificationAttributeModel model, bool continueEditing)
{
//try to get a product specification attribute with the specified id
var psa = await _specificationAttributeService.GetProductSpecificationAttributeByIdAsync(model.SpecificationId);
if (psa == null)
{
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
_notificationService.ErrorNotification("No product specification attribute found with the specified id");
return RedirectToAction("Edit", new { id = model.ProductId });
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null
&& (await _productService.GetProductByIdAsync(psa.ProductId)).VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification("This is not your product");
return RedirectToAction("List");
}
//we allow filtering and change option only for "Option" attribute type
//save localized values for CustomHtmlText and CustomText
switch (model.AttributeTypeId)
{
case (int)SpecificationAttributeType.Option:
psa.AllowFiltering = model.AllowFiltering;
psa.SpecificationAttributeOptionId = model.SpecificationAttributeOptionId;
break;
case (int)SpecificationAttributeType.CustomHtmlText:
psa.CustomValue = model.ValueRaw;
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(psa,
x => x.CustomValue,
localized.ValueRaw,
localized.LanguageId);
}
break;
case (int)SpecificationAttributeType.CustomText:
psa.CustomValue = model.Value;
foreach (var localized in model.Locales)
{
await _localizedEntityService.SaveLocalizedValueAsync(psa,
x => x.CustomValue,
localized.Value,
localized.LanguageId);
}
break;
default:
psa.CustomValue = model.Value;
break;
}
psa.ShowOnProductPage = model.ShowOnProductPage;
psa.DisplayOrder = model.DisplayOrder;
await _specificationAttributeService.UpdateProductSpecificationAttributeAsync(psa);
if (continueEditing)
{
return RedirectToAction("ProductSpecAttributeAddOrEdit",
new { productId = psa.ProductId, specificationId = model.SpecificationId });
}
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
return RedirectToAction("Edit", new { id = psa.ProductId });
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductSpecAttributeAddOrEdit(int productId, int? specificationId)
{
if (!specificationId.HasValue && !await _permissionService.AuthorizeAsync(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE))
return AccessDeniedView();
if (await _productService.GetProductByIdAsync(productId) == null)
{
_notificationService.ErrorNotification("No product found with the specified id");
return RedirectToAction("List");
}
//try to get a product specification attribute with the specified id
try
{
var model = await _productModelFactory.PrepareAddSpecificationAttributeModelAsync(productId, specificationId);
return View(model);
}
catch (Exception ex)
{
await _notificationService.ErrorNotificationAsync(ex);
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
return RedirectToAction("Edit", new { id = productId });
}
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductSpecAttrDelete(AddSpecificationAttributeModel model)
{
//try to get a product specification attribute with the specified id
var psa = await _specificationAttributeService.GetProductSpecificationAttributeByIdAsync(model.SpecificationId);
if (psa == null)
{
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
_notificationService.ErrorNotification("No product specification attribute found with the specified id");
return RedirectToAction("Edit", new { id = model.ProductId });
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && (await _productService.GetProductByIdAsync(psa.ProductId)).VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification("This is not your product");
return RedirectToAction("List", new { id = model.ProductId });
}
await _specificationAttributeService.DeleteProductSpecificationAttributeAsync(psa);
//select an appropriate card
SaveSelectedCardName("product-specification-attributes");
return RedirectToAction("Edit", new { id = psa.ProductId });
}
#endregion
#region Product tags
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)]
public virtual async Task<IActionResult> ProductTags()
{
//prepare model
var model = await _productModelFactory.PrepareProductTagSearchModelAsync(new ProductTagSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)]
public virtual async Task<IActionResult> ProductTags(ProductTagSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareProductTagListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductTagDelete(int id)
{
//try to get a product tag with the specified id
var tag = await _productTagService.GetProductTagByIdAsync(id)
?? throw new ArgumentException("No product tag found with the specified id");
await _productTagService.DeleteProductTagAsync(tag);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.ProductTags.Deleted"));
return RedirectToAction("ProductTags");
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductTagsDelete(ICollection<int> selectedIds)
{
if (selectedIds == null || !selectedIds.Any())
return NoContent();
var tags = await _productTagService.GetProductTagsByIdsAsync(selectedIds.ToArray());
await _productTagService.DeleteProductTagsAsync(tags);
return Json(new { Result = true });
}
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_VIEW)]
public virtual async Task<IActionResult> EditProductTag(int id)
{
//try to get a product tag with the specified id
var productTag = await _productTagService.GetProductTagByIdAsync(id);
if (productTag == null)
return RedirectToAction("List");
//prepare tag model
var model = await _productModelFactory.PrepareProductTagModelAsync(null, productTag);
return View(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCT_TAGS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> EditProductTag(ProductTagModel model, bool continueEditing)
{
//try to get a product tag with the specified id
var productTag = await _productTagService.GetProductTagByIdAsync(model.Id);
if (productTag == null)
return RedirectToAction("List");
if (ModelState.IsValid)
{
productTag.Name = model.Name;
await _productTagService.UpdateProductTagAsync(productTag);
//locales
await UpdateLocalesAsync(productTag, model);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.ProductTags.Updated"));
return continueEditing ? RedirectToAction("EditProductTag", new { id = productTag.Id }) : RedirectToAction("ProductTags");
}
//prepare model
model = await _productModelFactory.PrepareProductTagModelAsync(model, productTag, true);
//if we got this far, something failed, redisplay form
return View(model);
}
#endregion
#region Purchased with order
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> PurchasedWithOrders(ProductOrderSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductOrderListModelAsync(searchModel, product);
return Json(model);
}
#endregion
#region Export / Import
[HttpPost, ActionName("DownloadCatalogPDF")]
[FormValueRequired("download-catalog-pdf")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> DownloadCatalogAsPdf(ProductSearchModel model)
{
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
model.SearchVendorId = currentVendor.Id;
}
var categoryIds = new List<int> { model.SearchCategoryId };
//include subcategories
if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0)
categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true));
//0 - all (according to "ShowHidden" parameter)
//1 - published only
//2 - unpublished only
bool? overridePublished = null;
if (model.SearchPublishedId == 1)
overridePublished = true;
else if (model.SearchPublishedId == 2)
overridePublished = false;
var products = await _productService.SearchProductsAsync(0,
categoryIds: categoryIds,
manufacturerIds: new List<int> { model.SearchManufacturerId },
storeId: model.SearchStoreId,
vendorId: model.SearchVendorId,
warehouseId: model.SearchWarehouseId,
productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
keywords: model.SearchProductName,
showHidden: true,
overridePublished: overridePublished);
try
{
byte[] bytes;
await using (var stream = new MemoryStream())
{
await _pdfService.PrintProductsToPdfAsync(stream, products);
bytes = stream.ToArray();
}
return File(bytes, MimeTypes.ApplicationPdf, "pdfcatalog.pdf");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
[HttpPost, ActionName("ExportToXml")]
[FormValueRequired("exportxml-all")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> ExportXmlAll(ProductSearchModel model)
{
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
model.SearchVendorId = currentVendor.Id;
}
var categoryIds = new List<int> { model.SearchCategoryId };
//include subcategories
if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0)
categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true));
//0 - all (according to "ShowHidden" parameter)
//1 - published only
//2 - unpublished only
bool? overridePublished = null;
if (model.SearchPublishedId == 1)
overridePublished = true;
else if (model.SearchPublishedId == 2)
overridePublished = false;
var products = await _productService.SearchProductsAsync(0,
categoryIds: categoryIds,
manufacturerIds: new List<int> { model.SearchManufacturerId },
storeId: model.SearchStoreId,
vendorId: model.SearchVendorId,
warehouseId: model.SearchWarehouseId,
productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
keywords: model.SearchProductName,
showHidden: true,
overridePublished: overridePublished);
try
{
var xml = await _exportManager.ExportProductsToXmlAsync(products);
return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "products.xml");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> ExportXmlSelected(string selectedIds)
{
var products = new List<Product>();
if (selectedIds != null)
{
var ids = selectedIds
.Split(_separator, StringSplitOptions.RemoveEmptyEntries)
.Select(x => Convert.ToInt32(x))
.ToArray();
products.AddRange(await _productService.GetProductsByIdsAsync(ids));
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
products = products.Where(p => p.VendorId == currentVendor.Id).ToList();
}
try
{
var xml = await _exportManager.ExportProductsToXmlAsync(products);
return File(Encoding.UTF8.GetBytes(xml), MimeTypes.ApplicationXml, "products.xml");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
[HttpPost, ActionName("ExportToExcel")]
[FormValueRequired("exportexcel-all")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> ExportExcelAll(ProductSearchModel model)
{
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
model.SearchVendorId = currentVendor.Id;
}
var categoryIds = new List<int> { model.SearchCategoryId };
//include subcategories
if (model.SearchIncludeSubCategories && model.SearchCategoryId > 0)
categoryIds.AddRange(await _categoryService.GetChildCategoryIdsAsync(parentCategoryId: model.SearchCategoryId, showHidden: true));
//0 - all (according to "ShowHidden" parameter)
//1 - published only
//2 - unpublished only
bool? overridePublished = null;
if (model.SearchPublishedId == 1)
overridePublished = true;
else if (model.SearchPublishedId == 2)
overridePublished = false;
var products = await _productService.SearchProductsAsync(0,
categoryIds: categoryIds,
manufacturerIds: new List<int> { model.SearchManufacturerId },
storeId: model.SearchStoreId,
vendorId: model.SearchVendorId,
warehouseId: model.SearchWarehouseId,
productType: model.SearchProductTypeId > 0 ? (ProductType?)model.SearchProductTypeId : null,
keywords: model.SearchProductName,
showHidden: true,
overridePublished: overridePublished);
try
{
var bytes = await _exportManager.ExportProductsToXlsxAsync(products);
return File(bytes, MimeTypes.TextXlsx, "products.xlsx");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> ExportExcelSelected(string selectedIds)
{
var products = new List<Product>();
if (selectedIds != null)
{
var ids = selectedIds
.Split(_separator, StringSplitOptions.RemoveEmptyEntries)
.Select(x => Convert.ToInt32(x))
.ToArray();
products.AddRange(await _productService.GetProductsByIdsAsync(ids));
}
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null)
{
products = products.Where(p => p.VendorId == currentVendor.Id).ToList();
}
try
{
var bytes = await _exportManager.ExportProductsToXlsxAsync(products);
return File(bytes, MimeTypes.TextXlsx, "products.xlsx");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_IMPORT_EXPORT)]
public virtual async Task<IActionResult> ImportExcel(IFormFile importexcelfile)
{
if (await _workContext.GetCurrentVendorAsync() != null && !_vendorSettings.AllowVendorsToImportProducts)
//a vendor can not import products
return AccessDeniedView();
try
{
if (importexcelfile != null && importexcelfile.Length > 0)
{
await _importManager.ImportProductsFromXlsxAsync(importexcelfile.OpenReadStream());
}
else
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Common.UploadFile"));
return RedirectToAction("List");
}
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.Imported"));
return RedirectToAction("List");
}
catch (Exception exc)
{
await _notificationService.ErrorNotificationAsync(exc);
return RedirectToAction("List");
}
}
#endregion
#region Tier prices
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> TierPriceList(TierPriceSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareTierPriceListModelAsync(searchModel, product);
return Json(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> TierPriceCreatePopup(int productId)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId)
?? throw new ArgumentException("No product found with the specified id");
//prepare model
var model = await _productModelFactory.PrepareTierPriceModelAsync(new TierPriceModel(), product, null);
return View(model);
}
[HttpPost]
[FormValueRequired("save")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> TierPriceCreatePopup(TierPriceModel model)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(model.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
if (ModelState.IsValid)
{
//fill entity from model
var tierPrice = model.ToEntity<TierPrice>();
tierPrice.ProductId = product.Id;
tierPrice.CustomerRoleId = model.CustomerRoleId > 0 ? model.CustomerRoleId : (int?)null;
await _productService.InsertTierPriceAsync(tierPrice);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareTierPriceModelAsync(model, product, null, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> TierPriceEditPopup(int id)
{
//try to get a tier price with the specified id
var tierPrice = await _productService.GetTierPriceByIdAsync(id);
if (tierPrice == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(tierPrice.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareTierPriceModelAsync(null, product, tierPrice);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> TierPriceEditPopup(TierPriceModel model)
{
//try to get a tier price with the specified id
var tierPrice = await _productService.GetTierPriceByIdAsync(model.Id);
if (tierPrice == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(tierPrice.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
if (ModelState.IsValid)
{
//fill entity from model
tierPrice = model.ToEntity(tierPrice);
tierPrice.CustomerRoleId = model.CustomerRoleId > 0 ? model.CustomerRoleId : (int?)null;
await _productService.UpdateTierPriceAsync(tierPrice);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareTierPriceModelAsync(model, product, tierPrice, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> TierPriceDelete(int id)
{
//try to get a tier price with the specified id
var tierPrice = await _productService.GetTierPriceByIdAsync(id)
?? throw new ArgumentException("No tier price found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(tierPrice.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
await _productService.DeleteTierPriceAsync(tierPrice);
return new NullJsonResult();
}
#endregion
#region Product attributes
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeMappingList(ProductAttributeMappingSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeMappingListModelAsync(searchModel, product);
return Json(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeMappingCreate(int productId)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product"));
return RedirectToAction("List");
}
//prepare model
var model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(new ProductAttributeMappingModel(), product, null);
return View(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeMappingCreate(ProductAttributeMappingModel model, bool continueEditing)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(model.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product"));
return RedirectToAction("List");
}
//ensure this attribute is not mapped yet
if ((await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id))
.Any(x => x.ProductAttributeId == model.ProductAttributeId))
{
//redisplay form
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExists"));
model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(model, product, null, true);
return View(model);
}
//insert mapping
var productAttributeMapping = model.ToEntity<ProductAttributeMapping>();
await _productAttributeService.InsertProductAttributeMappingAsync(productAttributeMapping);
await UpdateLocalesAsync(productAttributeMapping, model);
//predefined values
var predefinedValues = await _productAttributeService.GetPredefinedProductAttributeValuesAsync(model.ProductAttributeId);
foreach (var predefinedValue in predefinedValues)
{
var pav = new ProductAttributeValue
{
ProductAttributeMappingId = productAttributeMapping.Id,
AttributeValueType = AttributeValueType.Simple,
Name = predefinedValue.Name,
PriceAdjustment = predefinedValue.PriceAdjustment,
PriceAdjustmentUsePercentage = predefinedValue.PriceAdjustmentUsePercentage,
WeightAdjustment = predefinedValue.WeightAdjustment,
Cost = predefinedValue.Cost,
IsPreSelected = predefinedValue.IsPreSelected,
DisplayOrder = predefinedValue.DisplayOrder
};
await _productAttributeService.InsertProductAttributeValueAsync(pav);
//locales
var languages = await _languageService.GetAllLanguagesAsync(true);
//localization
foreach (var lang in languages)
{
var name = await _localizationService.GetLocalizedAsync(predefinedValue, x => x.Name, lang.Id, false, false);
if (!string.IsNullOrEmpty(name))
await _localizedEntityService.SaveLocalizedValueAsync(pav, x => x.Name, name, lang.Id);
}
}
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Added"));
if (!continueEditing)
{
//select an appropriate card
SaveSelectedCardName("product-product-attributes");
return RedirectToAction("Edit", new { id = product.Id });
}
return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id });
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeMappingEdit(int id)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(id)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product"));
return RedirectToAction("List");
}
//prepare model
var model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(null, product, productAttributeMapping);
return View(model);
}
[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeMappingEdit(ProductAttributeMappingModel model, bool continueEditing, IFormCollection form)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.Id)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
{
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("This is not your product"));
return RedirectToAction("List");
}
//ensure this attribute is not mapped yet
if ((await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id))
.Any(x => x.ProductAttributeId == model.ProductAttributeId && x.Id != productAttributeMapping.Id))
{
//redisplay form
_notificationService.ErrorNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExists"));
model = await _productModelFactory.PrepareProductAttributeMappingModelAsync(model, product, productAttributeMapping, true);
return View(model);
}
//fill entity from model
productAttributeMapping = model.ToEntity(productAttributeMapping);
await _productAttributeService.UpdateProductAttributeMappingAsync(productAttributeMapping);
await UpdateLocalesAsync(productAttributeMapping, model);
await SaveConditionAttributesAsync(productAttributeMapping, model.ConditionModel, form);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Updated"));
if (!continueEditing)
{
//select an appropriate card
SaveSelectedCardName("product-product-attributes");
return RedirectToAction("Edit", new { id = product.Id });
}
return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeMappingDelete(int id)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(id)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//check if existed combinations contains the specified attribute
var existedCombinations = await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id);
if (existedCombinations?.Any() == true)
{
foreach (var combination in existedCombinations)
{
var mappings = await _productAttributeParser
.ParseProductAttributeMappingsAsync(combination.AttributesXml);
if (mappings?.Any(m => m.Id == productAttributeMapping.Id) == true)
{
_notificationService.ErrorNotification(
string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.AlreadyExistsInCombination"),
await _productAttributeFormatter.FormatAttributesAsync(product, combination.AttributesXml, await _workContext.GetCurrentCustomerAsync(), await _storeContext.GetCurrentStoreAsync(), ", ")));
return RedirectToAction("ProductAttributeMappingEdit", new { id = productAttributeMapping.Id });
}
}
}
await _productAttributeService.DeleteProductAttributeMappingAsync(productAttributeMapping);
_notificationService.SuccessNotification(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Deleted"));
//select an appropriate card
SaveSelectedCardName("product-product-attributes");
return RedirectToAction("Edit", new { id = productAttributeMapping.ProductId });
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeValueList(ProductAttributeValueSearchModel searchModel)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(searchModel.ProductAttributeMappingId)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeValueListModelAsync(searchModel, productAttributeMapping);
return Json(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeValueCreatePopup(int productAttributeMappingId)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeMappingId)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeValueModelAsync(new ProductAttributeValueModel(), productAttributeMapping, null);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeValueCreatePopup(ProductAttributeValueModel model)
{
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(model.ProductAttributeMappingId);
if (productAttributeMapping == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
if (productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares)
{
//ensure valid color is chosen/entered
if (string.IsNullOrEmpty(model.ColorSquaresRgb))
ModelState.AddModelError(string.Empty, "Color is required");
try
{
//ensure color is valid (can be instantiated)
System.Drawing.ColorTranslator.FromHtml(model.ColorSquaresRgb);
}
catch (Exception exc)
{
ModelState.AddModelError(string.Empty, exc.Message);
}
}
//ensure a picture is uploaded
if (productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares && model.ImageSquaresPictureId == 0)
{
ModelState.AddModelError(string.Empty, "Image is required");
}
if (ModelState.IsValid)
{
//fill entity from model
var pav = model.ToEntity<ProductAttributeValue>();
pav.Quantity = model.CustomerEntersQty ? 1 : model.Quantity;
await _productAttributeService.InsertProductAttributeValueAsync(pav);
await UpdateLocalesAsync(pav, model);
await SaveAttributeValuePicturesAsync(product, pav, model);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareProductAttributeValueModelAsync(model, productAttributeMapping, null, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeValueEditPopup(int id)
{
//try to get a product attribute value with the specified id
var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(id);
if (productAttributeValue == null)
return RedirectToAction("List", "Product");
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId);
if (productAttributeMapping == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeValueModelAsync(null, productAttributeMapping, productAttributeValue);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeValueEditPopup(ProductAttributeValueModel model)
{
//try to get a product attribute value with the specified id
var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(model.Id);
if (productAttributeValue == null)
return RedirectToAction("List", "Product");
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId);
if (productAttributeMapping == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
if (productAttributeMapping.AttributeControlType == AttributeControlType.ColorSquares)
{
//ensure valid color is chosen/entered
if (string.IsNullOrEmpty(model.ColorSquaresRgb))
ModelState.AddModelError(string.Empty, "Color is required");
try
{
//ensure color is valid (can be instantiated)
System.Drawing.ColorTranslator.FromHtml(model.ColorSquaresRgb);
}
catch (Exception exc)
{
ModelState.AddModelError(string.Empty, exc.Message);
}
}
//ensure a picture is uploaded
if (productAttributeMapping.AttributeControlType == AttributeControlType.ImageSquares && model.ImageSquaresPictureId == 0)
{
ModelState.AddModelError(string.Empty, "Image is required");
}
if (ModelState.IsValid)
{
//fill entity from model
productAttributeValue = model.ToEntity(productAttributeValue);
productAttributeValue.Quantity = model.CustomerEntersQty ? 1 : model.Quantity;
await _productAttributeService.UpdateProductAttributeValueAsync(productAttributeValue);
await UpdateLocalesAsync(productAttributeValue, model);
await SaveAttributeValuePicturesAsync(product, productAttributeValue, model);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareProductAttributeValueModelAsync(model, productAttributeMapping, productAttributeValue, true);
//if we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeValueDelete(int id)
{
//try to get a product attribute value with the specified id
var productAttributeValue = await _productAttributeService.GetProductAttributeValueByIdAsync(id)
?? throw new ArgumentException("No product attribute value found with the specified id");
//try to get a product attribute mapping with the specified id
var productAttributeMapping = await _productAttributeService.GetProductAttributeMappingByIdAsync(productAttributeValue.ProductAttributeMappingId)
?? throw new ArgumentException("No product attribute mapping found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productAttributeMapping.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//check if existed combinations contains the specified attribute value
var existedCombinations = await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id);
if (existedCombinations?.Any() == true)
{
foreach (var combination in existedCombinations)
{
var attributeValues = await _productAttributeParser.ParseProductAttributeValuesAsync(combination.AttributesXml);
if (attributeValues.Where(attribute => attribute.Id == id).Any())
{
return Conflict(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.AlreadyExistsInCombination"),
await _productAttributeFormatter.FormatAttributesAsync(product, combination.AttributesXml, await _workContext.GetCurrentCustomerAsync(), await _storeContext.GetCurrentStoreAsync(), ", ")));
}
}
}
await _productAttributeService.DeleteProductAttributeValueAsync(productAttributeValue);
return new NullJsonResult();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociateProductToAttributeValuePopup()
{
//prepare model
var model = await _productModelFactory.PrepareAssociateProductToAttributeValueSearchModelAsync(new AssociateProductToAttributeValueSearchModel());
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociateProductToAttributeValuePopupList(AssociateProductToAttributeValueSearchModel searchModel)
{
//prepare model
var model = await _productModelFactory.PrepareAssociateProductToAttributeValueListModelAsync(searchModel);
return Json(model);
}
[HttpPost]
[FormValueRequired("save")]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> AssociateProductToAttributeValuePopup([Bind(Prefix = nameof(AssociateProductToAttributeValueModel))] AssociateProductToAttributeValueModel model)
{
//try to get a product with the specified id
var associatedProduct = await _productService.GetProductByIdAsync(model.AssociatedToProductId);
if (associatedProduct == null)
return Content("Cannot load a product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && associatedProduct.VendorId != currentVendor.Id)
return Content("This is not your product");
ViewBag.RefreshPage = true;
ViewBag.productId = associatedProduct.Id;
ViewBag.productName = associatedProduct.Name;
return View(new AssociateProductToAttributeValueSearchModel());
}
//action displaying notification (warning) to a store owner when associating some product
public virtual async Task<IActionResult> AssociatedProductGetWarnings(int productId)
{
var associatedProduct = await _productService.GetProductByIdAsync(productId);
if (associatedProduct == null)
return Json(new { Result = string.Empty });
//attributes
if (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(associatedProduct.Id) is IList<ProductAttributeMapping> mapping && mapping.Any())
{
if (mapping.Any(attribute => attribute.IsRequired))
return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.HasRequiredAttributes") });
return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.HasAttributes") });
}
//gift card
if (associatedProduct.IsGiftCard)
{
return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.GiftCard") });
}
//downloadable product
if (associatedProduct.IsDownload)
{
return Json(new { Result = await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.Attributes.Values.Fields.AssociatedProduct.Downloadable") });
}
return Json(new { Result = string.Empty });
}
#endregion
#region Product attribute combinations
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeCombinationList(ProductAttributeCombinationSearchModel searchModel)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeCombinationListModelAsync(searchModel, product);
return Json(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeCombinationDelete(int id)
{
//try to get a combination with the specified id
var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(id)
?? throw new ArgumentException("No product attribute combination found with the specified id");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(combination.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
await _productAttributeService.DeleteProductAttributeCombinationAsync(combination);
return new NullJsonResult();
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeCombinationCreatePopup(int productId)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId);
if (product == null)
return RedirectToAction("List", "Product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(new ProductAttributeCombinationModel(), product, null);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
public virtual async Task<IActionResult> ProductAttributeCombinationCreatePopup(int productId, ProductAttributeCombinationModel model, IFormCollection form)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId);
if (product == null)
return RedirectToAction("List", "Product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//attributes
var warnings = new List<string>();
var attributesXml = await GetAttributesXmlForProductAttributeCombinationAsync(form, warnings, product.Id);
//check whether the attribute value is specified
if (string.IsNullOrEmpty(attributesXml))
warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Alert.FailedValue"));
warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(),
ShoppingCartType.ShoppingCart, product, 1, attributesXml, true));
//check whether the same attribute combination already exists
var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
if (existingCombination != null)
warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.AlreadyExists"));
if (!warnings.Any())
{
//save combination
var combination = model.ToEntity<ProductAttributeCombination>();
//fill attributes
combination.AttributesXml = attributesXml;
await _productAttributeService.InsertProductAttributeCombinationAsync(combination);
await SaveAttributeCombinationPicturesAsync(product, combination, model);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, combination.StockQuantity, combination.StockQuantity,
message: await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Combination.Edit"), combinationId: combination.Id);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, null, true);
model.Warnings = warnings;
//if we got this far, something failed, redisplay form
return View(model);
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeCombinationGeneratePopup(int productId)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId);
if (product == null)
return RedirectToAction("List", "Product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(new ProductAttributeCombinationModel(), product, null);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeCombinationGeneratePopup(IFormCollection form, ProductAttributeCombinationModel model)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(model.ProductId);
if (product == null)
return RedirectToAction("List", "Product");
var allowedAttributeIds = form.Keys.Where(key => key.Contains("attribute_value_"))
.Select(key => int.TryParse(form[key], out var id) ? id : 0).Where(id => id > 0).ToList();
var requiredAttributeNames = await (await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id))
.Where(pam => pam.IsRequired)
.Where(pam => !pam.IsNonCombinable())
.WhereAwait(async pam => !(await _productAttributeService.GetProductAttributeValuesAsync(pam.Id)).Any(v => allowedAttributeIds.Any(id => id == v.Id)))
.SelectAwait(async pam => (await _productAttributeService.GetProductAttributeByIdAsync(pam.ProductAttributeId)).Name).ToListAsync();
if (requiredAttributeNames.Any())
{
model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, null, true);
var pavModels = model.ProductAttributes.SelectMany(pa => pa.Values)
.Where(v => allowedAttributeIds.Any(id => id == v.Id))
.ToList();
foreach (var pavModel in pavModels)
{
pavModel.Checked = "checked";
}
model.Warnings.Add(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.SelectRequiredAttributes"), string.Join(", ", requiredAttributeNames)));
return View(model);
}
await GenerateAttributeCombinationsAsync(product, allowedAttributeIds);
ViewBag.RefreshPage = true;
return View(new ProductAttributeCombinationModel());
}
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeCombinationEditPopup(int id)
{
//try to get a combination with the specified id
var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(id);
if (combination == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(combination.ProductId);
if (product == null)
return RedirectToAction("List", "Product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//prepare model
var model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(null, product, combination);
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> ProductAttributeCombinationEditPopup(ProductAttributeCombinationModel model, IFormCollection form)
{
//try to get a combination with the specified id
var combination = await _productAttributeService.GetProductAttributeCombinationByIdAsync(model.Id);
if (combination == null)
return RedirectToAction("List", "Product");
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(combination.ProductId);
if (product == null)
return RedirectToAction("List", "Product");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return RedirectToAction("List", "Product");
//attributes
var warnings = new List<string>();
var attributesXml = await GetAttributesXmlForProductAttributeCombinationAsync(form, warnings, product.Id);
//check whether the attribute value is specified
if (string.IsNullOrEmpty(attributesXml))
warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.Alert.FailedValue"));
warnings.AddRange(await _shoppingCartService.GetShoppingCartItemAttributeWarningsAsync(await _workContext.GetCurrentCustomerAsync(),
ShoppingCartType.ShoppingCart, product, 1, attributesXml, true));
//check whether the same attribute combination already exists
var existingCombination = await _productAttributeParser.FindProductAttributeCombinationAsync(product, attributesXml);
if (existingCombination != null && existingCombination.Id != model.Id && existingCombination.AttributesXml.Equals(attributesXml))
warnings.Add(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ProductAttributes.AttributeCombinations.AlreadyExists"));
if (!warnings.Any() && ModelState.IsValid)
{
var previousStockQuantity = combination.StockQuantity;
//save combination
//fill entity from model
combination = model.ToEntity(combination);
combination.AttributesXml = attributesXml;
await _productAttributeService.UpdateProductAttributeCombinationAsync(combination);
await SaveAttributeCombinationPicturesAsync(product, combination, model);
//quantity change history
await _productService.AddStockQuantityHistoryEntryAsync(product, combination.StockQuantity - previousStockQuantity, combination.StockQuantity,
message: await _localizationService.GetResourceAsync("Admin.StockQuantityHistory.Messages.Combination.Edit"), combinationId: combination.Id);
ViewBag.RefreshPage = true;
return View(model);
}
//prepare model
model = await _productModelFactory.PrepareProductAttributeCombinationModelAsync(model, product, combination, true);
model.Warnings = warnings;
//if we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_CREATE_EDIT_DELETE)]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> GenerateAllAttributeCombinations(int productId)
{
//try to get a product with the specified id
var product = await _productService.GetProductByIdAsync(productId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
await GenerateAttributeCombinationsAsync(product);
return Json(new { Success = true });
}
#endregion
#region Product editor settings
[HttpPost]
[CheckPermission(StandardPermission.Configuration.MANAGE_SETTINGS)]
public virtual async Task<IActionResult> SaveProductEditorSettings(ProductModel model, string returnUrl = "")
{
//vendors cannot manage these settings
if (await _workContext.GetCurrentVendorAsync() != null)
return RedirectToAction("List");
var productEditorSettings = await _settingService.LoadSettingAsync<ProductEditorSettings>();
productEditorSettings = model.ProductEditorSettingsModel.ToSettings(productEditorSettings);
await _settingService.SaveSettingAsync(productEditorSettings);
//product list
if (string.IsNullOrEmpty(returnUrl))
return RedirectToAction("List");
//prevent open redirection attack
if (!Url.IsLocalUrl(returnUrl))
return RedirectToAction("List");
return Redirect(returnUrl);
}
#endregion
#region Stock quantity history
[HttpPost]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> StockQuantityHistory(StockQuantityHistorySearchModel searchModel)
{
var product = await _productService.GetProductByIdAsync(searchModel.ProductId)
?? throw new ArgumentException("No product found with the specified id");
//a vendor should have access only to his products
var currentVendor = await _workContext.GetCurrentVendorAsync();
if (currentVendor != null && product.VendorId != currentVendor.Id)
return Content("This is not your product");
//prepare model
var model = await _productModelFactory.PrepareStockQuantityHistoryListModelAsync(searchModel, product);
return Json(model);
}
#endregion
#endregion
#region Nested class
protected class BulkEditData
{
protected bool _updated;
protected bool _created;
protected int _defaultTaxCategoryId;
protected int _vendorId;
public BulkEditData(int defaultTaxCategoryId, int vendorId)
{
_defaultTaxCategoryId = defaultTaxCategoryId;
_vendorId = vendorId;
}
public bool IsSelected { get; set; }
public string Name { get; set; }
public string Sku { get; set; }
public decimal Price { get; set; }
public decimal OldPrice { get; set; }
public int Quantity { get; set; }
public bool IsPublished { get; set; }
public Product Product { get; set; }
public bool NeedToUpdate(bool selected)
{
if (selected && !IsSelected)
return false;
if (Product == null)
return false;
if (_updated)
return true;
return isStringValueChanged(Product.Name, Name) ||
isStringValueChanged(Product.Sku, Sku) ||
!Product.Price.Equals(Price) ||
!Product.OldPrice.Equals(OldPrice) ||
!Product.StockQuantity.Equals(Quantity) ||
!Product.Published.Equals(IsPublished);
bool isStringValueChanged(string oldValue, string newValue)
{
if (string.IsNullOrEmpty(oldValue))
return !string.IsNullOrEmpty(newValue);
return !oldValue.Equals(newValue);
}
}
public bool NeedToCreate(bool selected)
{
if (selected && !IsSelected)
return false;
if (Product != null)
return false;
return true;
}
public Product UpdateProduct(bool selected)
{
if (!NeedToUpdate(selected) || _updated)
return Product;
Product.Name = Name;
Product.Sku = Sku;
Product.Price = Price;
Product.OldPrice = OldPrice;
Product.StockQuantity = Quantity;
Product.Published = IsPublished;
_updated = true;
return Product;
}
public Product CreateProduct(bool selected)
{
if (!NeedToCreate(selected) || _created)
return Product;
Product = new Product
{
Name = Name,
Sku = Sku,
Price = Price,
OldPrice = OldPrice,
StockQuantity = Quantity,
Published = IsPublished,
//set default values for the new model
MaximumCustomerEnteredPrice = 1000,
MaxNumberOfDownloads = 10,
RecurringCycleLength = 100,
RecurringTotalCycles = 10,
RentalPriceLength = 1,
NotifyAdminForQuantityBelow = 1,
OrderMinimumQuantity = 1,
OrderMaximumQuantity = 10000,
TaxCategoryId = _defaultTaxCategoryId,
UnlimitedDownloads = true,
IsShipEnabled = true,
AllowCustomerReviews = true,
VisibleIndividually = true,
ProductType = ProductType.SimpleProduct,
VendorId = _vendorId
};
_created = true;
return Product;
}
}
#endregion
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace DevExtreme.NETCore.Demos.Controllers
{
public class FileManagerController : Controller
{
public IActionResult BindingToFileSystem()
{
return View();
}
}
}

View File

@ -0,0 +1,48 @@
using DevExtreme.AspNet.Mvc.FileManagement;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using System.Collections.Generic;
namespace DevExtreme.NETCore.Demos.Controllers
{
public class FileManagerScriptsApiController : Controller
{
IWebHostEnvironment _webHostEnvironment;
public FileManagerScriptsApiController(IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment = webHostEnvironment;
}
[HttpGet]
[HttpPost]
[Route("api/file-manager-file-system", Name = "FileManagementFileSystemApi")]
public object FileSystem(FileSystemCommand command, string arguments, int orderId)
{
string path = Request.Headers["TestHeader"];
var valami = new List<int>();
var config = new FileSystemConfiguration
{
Request = Request,
FileSystemProvider = new PhysicalFileSystemProvider(_webHostEnvironment.ContentRootPath + $"/wwwroot/uploads/orders/order{path}"),
//uncomment the code below to enable file/folder management
//AllowCopy = true,
//AllowCreate = true,
//AllowMove = true,
//AllowDelete = true,
//AllowRename = true,
//AllowUpload = true,
//AllowDownload = true,
AllowedFileExtensions = new[] { ".pdf", ".jpg", ".jpeg" }
};
var processor = new FileSystemCommandProcessor(config);
var result = processor.Execute(command, arguments);
return result.GetClientCommandResult();
}
}
}

View File

@ -24,12 +24,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
private readonly IPermissionService _permissionService;
protected readonly FruitBankDbContext _dbContext;
protected readonly AICalculationService _aiCalculationService;
protected readonly OpenAIApiService _openAIApiService;
public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService)
public ManagementPageController(IPermissionService permissionService, FruitBankDbContext fruitBankDbContext, AICalculationService aiCalculationService, OpenAIApiService openAIApiService)
{
_permissionService = permissionService;
_dbContext = fruitBankDbContext;
_aiCalculationService = aiCalculationService;
_openAIApiService = openAIApiService;
}
public async Task<IActionResult> Test()
@ -42,6 +44,31 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var testGridModel2 = new TestGridModel();
testGridModel2.GridName = "Orders";
testGridModel2.ViewComponentName = "ShippingDocumentGridComponent";
testGridModel2.ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/ShippingDocumentGridComponent.cshtml";
testGridModel2.Configuration = new GridConfiguration();
testGridModel2.Configuration.ShowChildGridsAsTabs = true;
testGridModel2.ChildGrids = new List<TestGridModel>();
var childGrid1 = new TestGridModel
{
GridName = "TestGrid",
ViewComponentName = "TestGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/TestGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid1);
var childGrid2 = new TestGridModel
{
GridName = "Files",
ViewComponentName = "FileUploadGridComponent",
ViewComponentLocation = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/FileUploadGridComponent.cshtml",
ParentGridId = testGridModel2.Id,
ChildGrids = new List<TestGridModel>()
};
testGridModel2.ChildGrids.Add(childGrid2);
testPageModel.Grids.Add(testGridModel2);
var testGridModel = new TestGridModel();
@ -58,6 +85,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/Test.cshtml", testPageModel);
}
[HttpPost]
public IActionResult LoadChildGrid([FromBody] LoadChildGridRequest request)
{
// request.contextId is the actual row ID (data.Id from DevExtreme)
// request.childModel is the full TestGridModel object
// Add the context ID to the model's DataContext
if (request.ChildModel.DataContext == null)
request.ChildModel.DataContext = new Dictionary<string, int>();
request.ChildModel.DataContext["contextId"] = request.ContextId;
// Invoke the view component with the full model
return ViewComponent(request.ChildModel.ViewComponentName, request.ChildModel);
}
// Request model for deserialization
public class LoadChildGridRequest
{
public int ContextId { get; set; } // The actual row ID from data.Id
public TestGridModel ChildModel { get; set; } // The full child grid model
}
[HttpGet]
public async Task<IActionResult> GetShippings()
{
@ -135,11 +186,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return Json(model);
}
[HttpPost]
[RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
{
var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId);
//checks
// - files exist
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -159,8 +213,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
}
var filesList = new List<Files>();
var shippingDocumentToFileList = new List<ShippingDocumentToFiles>();
//iteratation 1: iterate documents to determine their type by AI
@ -168,16 +221,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
foreach (var file in files)
{
var fileName = file.FileName;
var fileSize = file.Length;
var dbFile = new Files();
string pdfText = "";
Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}");
Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}, content type: {file.ContentType}");
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF files are allowed" });
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !file.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" });
// Validate file size (max 20MB)
if (file.Length > 20 * 1024 * 1024)
@ -185,38 +237,91 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// - get text extracted from pdf
// Validate file type (PDF only)
if (file.Length > 0 && file.ContentType == "application/pdf")
//if (file.Length > 0 && file.ContentType == "application/pdf")
if (file.Length > 0)
{
try
{
// Open the PDF from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream))
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){
try
{
// Now you can analyze the PDF content
foreach (var page in pdf.GetPages())
// Open the PDF from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream))
{
// Extract text from each page
pdfText += ContentOrderTextExtractor.GetText(page);
// Now you can analyze the PDF content
foreach (var page in pdf.GetPages())
{
// Extract text from each page
pdfText += ContentOrderTextExtractor.GetText(page);
}
//still nothing? let's send it to AI
if (string.IsNullOrWhiteSpace(pdfText))
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
}
// For demonstration, let's just log the extracted text
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
}
// For demonstration, let's just log the extracted text
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
}
catch (Exception ex)
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
catch (Exception ex)
else
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
try
{
// Open the PDF from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
{
try
{
// ✅ Use the service we implemented earlier
pdfText = await _openAIApiService.AnalyzePdfAsync(stream, file.FileName, "Please extract all readable text from this PDF.");
}
catch (Exception aiEx)
{
Console.Error.WriteLine($"OpenAI Assistants API failed: {aiEx.Message}");
return StatusCode(500, $"Failed to process PDF: {aiEx.Message}");
}
// For demonstration, let's just log the extracted text
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
}
}
catch (Exception ex)
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
//we should have some kind of text now
Console.WriteLine(pdfText);
}
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document from the available list, and return them as JSON: documentNumber, documentType. " +
"document IN ENGLISH from the available list, and return them as JSON: documentNumber, documentType. " +
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" +
"If you can't find information of any of these, return null value for that field.";
@ -226,11 +331,22 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis);
if (extractedMetaData.DocumentNumber != null)
{
dbFile.RawText = pdfText;
dbFile.FileExtension = "pdf";
dbFile.FileName = extractedMetaData.DocumentNumber;
dbFile.FileExtension = "pdf";
dbFile.FileName = extractedMetaData.DocumentNumber;
}
await _dbContext.Files.InsertAsync(dbFile);
filesList.Add(dbFile);
ShippingDocumentToFiles shippingDocumentToFiles = new ShippingDocumentToFiles
{
ShippingDocumentId = shippingDocumentId,
FilesId = dbFile.Id
};
await _dbContext.ShippingDocumentToFiles.InsertAsync(shippingDocumentToFiles);
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner)
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
@ -254,7 +370,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Console.WriteLine(extractedPartnerData.TaxId);
}
if (extractedPartnerData.Country != null) {
if (extractedPartnerData.Country != null)
{
Console.WriteLine(extractedPartnerData.Country);
}

View File

@ -0,0 +1,10 @@
using Nop.Web.Framework.Models;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.Catalog;
/// <summary>
/// Represents a product list model
/// </summary>
public partial record ProductListModel : BasePagedListModel<ProductModel>
{
}

View File

@ -1,18 +1,185 @@
//using Nop.Web.Framework.Models;
//using Nop.Web.Framework.Mvc.ModelBinding;
//using System.ComponentModel.DataAnnotations;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
/// <summary>
/// Represents a grid with potential child grids
/// </summary>
public class TestGridModel
{
public Guid Id = Guid.NewGuid();
public TestGridModel()
{
Id = Guid.NewGuid();
ChildGrids = new List<TestGridModel>();
Configuration = new GridConfiguration();
}
// Identity
public Guid Id { get; set; }
public string GridName { get; set; }
//public string GridControllerName { get; set; }
//public string GridEndpointName { get; set; }
// View Component Information
public string ViewComponentName { get; set; }
public string ViewComponentLocation { get; set; }
public int? ParentRowId { get; set; }
// Hierarchy
public Guid? ParentGridId { get; set; }
public int Level { get; set; } // 0 = top level, 1 = first nested, etc.
public List<TestGridModel> ChildGrids { get; set; }
// Grid Behavior Configuration
public GridConfiguration Configuration { get; set; }
// Data Context (optional - for passing entity IDs or filter params)
public Dictionary<string, int> DataContext { get; set; }
}
/// <summary>
/// Configuration for grid rendering and behavior
/// </summary>
public class GridConfiguration
{
// Display Settings
public bool ShowChildGridsAsTabs { get; set; } = true;
public bool ShowChildGridsAsAccordion { get; set; } = false;
public bool ShowChildGridsInline { get; set; } = false;
// Rendering Options
public string ChildGridContainerCssClass { get; set; } = "nested-grid-container";
public bool LazyLoadChildren { get; set; } = false;
public bool CollapseByDefault { get; set; } = false;
// Data Loading
public string ChildDataEndpoint { get; set; }
public bool RequiresParentRowSelection { get; set; } = false;
// Metadata
public string Description { get; set; }
public int DisplayOrder { get; set; }
}
/// <summary>
/// Builder class for easier model construction
/// </summary>
public class TestPageModelBuilder
{
private readonly TestPageModel _model;
private readonly Dictionary<Guid, TestGridModel> _gridLookup;
public TestPageModelBuilder()
{
_model = new TestPageModel();
_gridLookup = new Dictionary<Guid, TestGridModel>();
}
public TestPageModelBuilder AddRootGrid(TestGridModel grid)
{
grid.Level = 0;
grid.ParentGridId = null;
_model.Grids.Add(grid);
_gridLookup[grid.Id] = grid;
return this;
}
public TestPageModelBuilder AddChildGrid(Guid parentId, TestGridModel childGrid)
{
if (_gridLookup.TryGetValue(parentId, out var parentGrid))
{
childGrid.Level = parentGrid.Level + 1;
childGrid.ParentGridId = parentId;
parentGrid.ChildGrids.Add(childGrid);
_gridLookup[childGrid.Id] = childGrid;
}
return this;
}
public TestPageModel Build()
{
return _model;
}
}
/// <summary>
/// Example usage helper
/// </summary>
public static class TestPageModelExample
{
public static TestPageModel CreateSampleModel()
{
var builder = new TestPageModelBuilder();
// Level 0 - Root Grids
var customersGrid = new TestGridModel
{
GridName = "Customers",
ViewComponentName = "CustomerGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = false,
Description = "Main customer list"
}
};
var ordersRootGrid = new TestGridModel
{
GridName = "All Orders",
ViewComponentName = "OrderGrid"
};
builder.AddRootGrid(customersGrid);
builder.AddRootGrid(ordersRootGrid);
// Level 1 - Child of Customers
var customerOrdersGrid = new TestGridModel
{
GridName = "Customer Orders",
ViewComponentName = "CustomerOrderGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = true,
ShowChildGridsAsTabs = true
}
};
var customerAddressesGrid = new TestGridModel
{
GridName = "Addresses",
ViewComponentName = "CustomerAddressGrid"
};
builder.AddChildGrid(customersGrid.Id, customerOrdersGrid);
builder.AddChildGrid(customersGrid.Id, customerAddressesGrid);
// Level 2 - Child of Customer Orders
var orderItemsGrid = new TestGridModel
{
GridName = "Order Items",
ViewComponentName = "OrderItemGrid",
Configuration = new GridConfiguration
{
RequiresParentRowSelection = true
}
};
var orderShipmentsGrid = new TestGridModel
{
GridName = "Shipments",
ViewComponentName = "OrderShipmentGrid"
};
builder.AddChildGrid(customerOrdersGrid.Id, orderItemsGrid);
builder.AddChildGrid(customerOrdersGrid.Id, orderShipmentsGrid);
// Level 3 - Child of Order Items
var itemAttributesGrid = new TestGridModel
{
GridName = "Item Attributes",
ViewComponentName = "ItemAttributeGrid"
};
builder.AddChildGrid(orderItemsGrid.Id, itemAttributesGrid);
return builder.Build();
}
}
}

View File

@ -6,6 +6,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
{
public class TestPageModel
{
public TestPageModel()
{
Grids = new List<TestGridModel>();
}
public List<TestGridModel> Grids { get; set; }
}
}

View File

@ -0,0 +1,112 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using DevExtreme.AspNet.Mvc
@{
var contextId = Model.DataContext["contextId"];
var fileManagerId = $"fileManager_{contextId}";
var beforeAjaxSendFunctionName = $"beforeAjaxSend_{contextId}";
}
<div class="row">
<div class="col-6">
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
@(Html.DevExtreme().FileUploader()
.ID("shippingDocumentUploader-" + contextId)
.Name("files")
.Multiple(true)
.Accept("application/pdf")
.UploadMode(FileUploadMode.UseForm)
)
<input type="hidden" name="ShippingDocumentId" value="@contextId" />
<input type="hidden" name="PartnerId" value="hello" />
@(Html.DevExtreme().Button()
.Text("Upload Files")
.Type(ButtonType.Success)
.UseSubmitBehavior(true)
)
</form>
<div class="content" id="selected-files">
<div>
<h4>Selected Files</h4>
</div>
</div>
</div>
<div class="col-6">
@(Html.DevExtreme().FileManager()
.ID(fileManagerId)
.FileSystemProvider(provider => provider.Remote()
.Url(Url.RouteUrl("FileManagementFileSystemApi"))
.BeforeAjaxSend(@<text>
function(arg) {
arg.headers.TestHeader = @Model.DataContext["contextId"];
}
</text>))
.Permissions(permissions => {
permissions.Download(true);
permissions.Upload(true);
})
.AllowedFileExtensions(new[] { ".pdf", ".jpg", ".jpeg" })
)
</div>
</div>
<script>
function fileUploader_valueChanged(e) {
var files = e.value;
if(files.length > 0) {
$("#selected-files .selected-item").remove();
$.each(files, function(i, file) {
var $selectedItem = $("<div />").addClass("selected-item");
$selectedItem.append(
$("<span />").html("Name: " + file.name + "<br/>"),
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
$("<span />").html("Type " + file.type + "<br/>"),
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
);
$selectedItem.appendTo($("#selected-files"));
});
$("#selected-files").show();
}
else
$("#selected-files").hide();
}
function getGridInstance() {
return $("#shippingDocumentUploader").dxFileUploader("instance");
}
function fileUploader_fileUploaded(e) {
const fileUploaderId = e.component.element().attr('id');
// 2. Extract the number from the ID
const match = fileUploaderId.match(/\d+$/);
if (match) {
const uniqueId = match[0];
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
// 3. Get the DevExtreme grid instance and refresh it
const grid = $(`#${gridId}`).dxDataGrid('instance');
if (grid) {
grid.dxDataGrid("getDataSource").reload();
// Optional: Show a success notification
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
} else {
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
}
} else {
console.error("Could not find a unique ID number from the file uploader.");
}
// shippingDocumentGridContainer
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
}
</script>

View File

@ -1,6 +1,7 @@
@model OrderSearchModel
@inject IStoreService storeService
@using FruitBank.Common.Interfaces
@using Nop.Plugin.Misc.FruitBankPlugin.Models
@using Nop.Services.Stores
@using Nop.Web.Areas.Admin.Components
@ -290,9 +291,7 @@
</div>
<div class="card card-default">
<div class="card-header">
<h1>RTTTTTTTTTT</h1>
</div>
<div class="card-body">
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Orders", Docs.Orders + Utm.OnAdmin)" />
@ -342,11 +341,18 @@
};
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
{
Title = "Needs Measurement",
Title = T("Admin.Orders.Fields.ToBeMeasured").Text,
Width = "100",
Render = new RenderCustom("renderColumnNeedsMeasurement"),
Render = new RenderCustom("renderColumnIsMeasurable"),
ClassName = NopColumnClassDefaults.CenterAll
});
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(IOrderDto.DateOfReceipt))
{
Title = T("Admin.Orders.Fields.PickupDate").Text,
Width = "100",
Render = new RenderCustom("renderColumnPickupDateAndTime"),
ClassName = NopColumnClassDefaults.CenterAll
});
//a vendor does not have access to this functionality
if (!Model.IsLoggedInAsVendor)
@ -441,11 +447,16 @@
return `${textRenderer(row.CustomerFullName)} <br /><a href="${link}">${data}</a > `;
}
function renderColumnNeedsMeasurement(data, type, row, meta) {
function renderColumnIsMeasurable(data, type, row, meta) {
if(data === true) {
return '<span class="badge badge-warning">Yes</span>';
return '<span class="badge badge-warning" disabled>Yes</span>';
}
return '<span class="badge badge-secondary">No</span>';
return '<span class="badge badge-secondary" disabled>No</span>';
}
function renderColumnPickupDateAndTime(data, type, row, meta) {
return `<span>${data}</span>`;
}
$(function() {

View File

@ -1,10 +1,6 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using DevExtreme.AspNet.Mvc
@{
var contextId = Model;
// var gridId = $"dataGrid_{Guid.NewGuid():N}";
}
@Html.AntiForgeryToken()
<div>
@(
@ -27,23 +23,21 @@
editing.AllowDeleting(true);
})
.Columns(c => {
c.Add().DataField("Id").AllowEditing(false);
c.Add().DataField("Partner.Name").AllowEditing(false);
c.Add()
.Caption("Items in order")
.DataType(GridColumnDataType.Number)
.CalculateCellValue("calculateItemsCount").AllowEditing(false);
.Caption("Items in order")
.DataType(GridColumnDataType.Number)
.CalculateCellValue("calculateItemsCount").AllowEditing(false);
c.Add().DataField("PartnerId");
c.Add().DataField("DocumentIdNumber");
c.Add().DataField("IsAllMeasured");
c.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
.CalculateCellValue("calculateCellValue").AllowEditing(false);
})
.Toolbar(toolbar => {
c.Add().DataField("DocumentIdNumber");
c.Add().DataField("IsAllMeasured");
c.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
.CalculateCellValue("calculateCellValue").AllowEditing(false);
})
.Toolbar(toolbar => {
toolbar.Items(items => {
items.Add()
.Name("addRowButton")
@ -60,244 +54,146 @@
);
});
})
.MasterDetail(md => {
md.Enabled(true);
md.Template(@<text>
<div class="master-detail-caption"><%- data.ShippingDate %> <%- data.LicencePlate %>'s shippingdocuments:</div>
<div id="fileuploader">
<div class="widget-container">
<section id="tabs" class="project-tab">
<div class="row">
<div class="col-md-12">
<nav>
<div class="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-home-tab" data-toggle="tab" href="#nav-home" role="tab" aria-controls="nav-home" aria-selected="true">Partner info</a>
<a class="nav-item nav-link" id="nav-profile-tab" data-toggle="tab" href="#nav-profile" role="tab" aria-controls="nav-profile" aria-selected="false">Products</a>
<a class="nav-item nav-link" id="nav-contact-tab" data-toggle="tab" href="#nav-contact" role="tab" aria-controls="nav-contact" aria-selected="false">Files</a>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">
<table class="table" cellspacing="0">
<thead>
<tr>
<th>Project Name</th>
<th>Employer</th>
<th>Awards</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#">Work 1</a></td>
<td>Doe</td>
<td>john@example.com</td>
</tr>
<tr>
<td><a href="#">Work 2</a></td>
<td>Moe</td>
<td>mary@example.com</td>
</tr>
<tr>
<td><a href="#">Work 3</a></td>
<td>Dooley</td>
<td>july@example.com</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">
<table class="table" cellspacing="0">
<thead>
<tr>
<th>Project Name</th>
<th>Employer</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#">Work 1</a></td>
<td>Doe</td>
<td>john@example.com</td>
</tr>
<tr>
<td><a href="#">Work 2</a></td>
<td>Moe</td>
<td>mary@example.com</td>
</tr>
<tr>
<td><a href="#">Work 3</a></td>
<td>Dooley</td>
<td>july@example.com</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-contact" role="tabpanel" aria-labelledby="nav-contact-tab">
<form method="post" enctype="multipart/form-data" asp-controller="ManagementPage" asp-action="UploadFile">
@(Html.DevExtreme().FileUploader()
.ID(new JS("'shippingDocumentUploader-' + data.Id"))
.Name("files")
.Multiple(true)
.Accept("application/pdf")
.UploadMode(FileUploadMode.UseForm)
.MasterDetail(md => md.Enabled(true).Template(new TemplateName("masterDetailTemplate")))
)
<input type="hidden" name="ShippingDocumentId" value="<%- data.Id %>" />
<% if (data.PartnerId) { %>
<input type="hidden" name="PartnerId" value="<%- data.PartnerId %>" />
<% } %>
@(Html.DevExtreme().Button()
.Text("Upload Files")
.Type(ButtonType.Success)
.UseSubmitBehavior(true)
)
</form>
<div class="content" id="selected-files">
<div>
<h4>Selected Files</h4>
</div>
</div>
@* <div id="@(new JS("'shippingDocumentGridContainer-' + data.ID"))"></div>
@(Html.DevExtreme().DataGrid<FruitBank.Common.Entities.ShippingDocument>()
.ColumnAutoWidth(true)
.ShowBorders(true)
.ID(new JS("'shippingDocumentGridContainer-' + data.Id"))
.Columns(columns => {
columns.AddFor(m => m.Id).AllowEditing(false);
columns.AddFor(m => m.Country);
columns.AddFor(m => m.Created);
columns.AddFor(m => m.PartnerId);
columns.Add()
.Caption("Completed")
.DataType(GridColumnDataType.Boolean)
.CalculateCellValue("calculateCellValue");
})
.DataSource(ds => ds.Mvc()
.Controller("Shipping")
.LoadAction("GetShippingDocumentsByShippingId")
.LoadParams(new { shippingId = new JS("data.Id") })
.Key("Id")
)
) *@
@using (Html.DevExtreme().NamedTemplate("masterDetailTemplate"))
{
<div class="master-detail-caption">
<%- data.ShippingDate %> <%- data.LicencePlate %>'s shipping documents:
</div>
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
{
<div class="@Model.Configuration.ChildGridContainerCssClass mt-3" data-parent-id="<%- data.Id %>">
@if (Model.Configuration.ShowChildGridsAsTabs)
{
<ul class="nav nav-tabs" role="tablist">
@for (int i = 0; i < Model.ChildGrids.Count; i++)
{
var child = Model.ChildGrids[i];
var isActive = i == 0 ? "active" : "";
<li class="nav-item">
<a class="nav-link @isActive"
data-toggle="tab"
data-child-index="@i"
href="#content-@child.Id-<%- data.Id %>"
onclick="reloadChildGrid(this, '<%- data.Id %>', @i)">
@child.GridName
</a>
</li>
}
</ul>
<div class="tab-content mt-2">
@for (int i = 0; i < Model.ChildGrids.Count; i++)
{
var child = Model.ChildGrids[i];
var isActive = i == 0 ? "show active" : "";
<div class="tab-pane fade @isActive"
id="content-@child.Id-<%- data.Id %>"
data-child-index="@i">
@* Initial load - will be replaced by AJAX on first tab click *@
<div class="text-center p-3">
<span class="spinner-border spinner-border-sm" role="status"></span>
Loading...
</div>
</div>
</div>
}
</div>
</section>
}
</div>
</div>
</text>);
})
)
</div>
}
}
<script>
// Store the parent grid model as JSON
var parentGridModel = @Html.Raw(Json.Serialize(Model));
// Global function to reload child grids
function reloadChildGrid(tabElement, contextId, childIndex) {
const $tab = $(tabElement);
const $contentPane = $($tab.attr('href'));
// Check if already loaded
if ($contentPane.data('loaded')) {
return;
}
// Show loading state
$contentPane.html('<div class="text-center p-3"><span class="spinner-border spinner-border-sm"></span> Loading...</div>');
// Get the child model from the parent model
var childModel = parentGridModel.ChildGrids[childIndex];
$.ajax({
url: '@Url.Action("LoadChildGrid", "ManagementPage")',
type: 'POST',
contentType: 'application/json',
headers: {
'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
},
data: JSON.stringify({
contextId: contextId,
childModel: childModel
}),
success: function (data) {
$contentPane.html(data);
$contentPane.data('loaded', true);
},
error: function (xhr, status, error) {
$contentPane.html('<div class="alert alert-danger">Error loading grid: ' + error + '</div>');
}
});
}
// Load first tab automatically when master detail opens
$(document).ready(function() {
$('#orderDataGridContainer').on('contentReady', function() {
// This will trigger when master detail rows are expanded
$('.nav-tabs .nav-link.active').each(function() {
const contextId = $(this).closest('[data-parent-id]').attr('data-parent-id');
const childIndex = $(this).data('child-index');
if (contextId && childIndex !== undefined) {
reloadChildGrid(this, contextId, childIndex);
}
});
});
});
function calculateCellValue(rowData) {
return rowData.Status === "Completed";
}
</script>
<script>
function calculateItemsCount(rowData) {
return rowData.ShippingItems.length;
return rowData.ShippingItems ? rowData.ShippingItems.length : 0;
}
</script>
<script>
function onDeleteBtnClick(){
let dataGrid = $("#gridContainer").dxDataGrid("instance");
function onDeleteBtnClick() {
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
$.when.apply($, dataGrid.getSelectedRowsData().map(function(data) {
return dataGrid.getDataSource().store().remove(data.ID);
return dataGrid.getDataSource().store().remove(data.Id);
})).done(function() {
dataGrid.refresh();
});
}
function calculateFilterExpression(filterValue, selectedFilterOperation, target) {
if(target === "search" && typeof(filterValue) === "string") {
return [this.dataField, "contains", filterValue]
}
return function(data) {
return (data.AssignedEmployee || []).indexOf(filterValue) !== -1
}
}
function onSelectionChanged(data) {
let dataGrid = $("#gridContainer").dxDataGrid("instance");
let dataGrid = $("#orderDataGridContainer").dxDataGrid("instance");
dataGrid.option("toolbar.items[1].options.disabled", !data.selectedRowsData.length);
}
function onRowExpanded(e) {
e.component.dxDataGrid("getDataSource").reload();
}
</script>
<script>
function fileUploader_valueChanged(e) {
var files = e.value;
if(files.length > 0) {
$("#selected-files .selected-item").remove();
$.each(files, function(i, file) {
var $selectedItem = $("<div />").addClass("selected-item");
$selectedItem.append(
$("<span />").html("Name: " + file.name + "<br/>"),
$("<span />").html("Size " + file.size + " bytes" + "<br/>"),
$("<span />").html("Type " + file.type + "<br/>"),
$("<span />").html("Last Modified Date: " + file.lastModifiedDate)
);
$selectedItem.appendTo($("#selected-files"));
});
$("#selected-files").show();
// Trigger loading of first tab when row expands
const $firstTab = $(e.element).find('.master-detail-caption').next().find('.nav-link.active').first();
if ($firstTab.length) {
const contextId = e.key; // This is the actual row's Id (data.Id)
const childIndex = $firstTab.data('child-index');
if (contextId && childIndex !== undefined) {
setTimeout(() => reloadChildGrid($firstTab[0], contextId, childIndex), 100);
}
}
else
$("#selected-files").hide();
}
function getGridInstance() {
return $("#shippingDocumentUploader").dxFileUploader("instance");
}
function fileUploader_fileUploaded(e) {
const fileUploaderId = e.component.element().attr('id');
// 2. Extract the number from the ID
const match = fileUploaderId.match(/\d+$/);
if (match) {
const uniqueId = match[0];
const gridId = `shippingDocumentGridContainer-${uniqueId}`;
// 3. Get the DevExtreme grid instance and refresh it
const grid = $(`#${gridId}`).dxDataGrid('instance');
if (grid) {
grid.dxDataGrid("getDataSource").reload();
// Optional: Show a success notification
DevExpress.ui.notify("Documents updated successfully!", "success", 2000);
} else {
console.error(`DevExtreme grid with ID "${gridId}" not found.`);
}
} else {
console.error("Could not find a unique ID number from the file uploader.");
}
// shippingDocumentGridContainer
//$("#shippingDocumentGridContainer" + e.component.ID).dxDataGrid("getDataSource").reload();
}
</script>
</script>

View File

@ -0,0 +1,117 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
@using System.Text.Json
<div class="card">
<div class="card-header bg-info text-white">
<h4>Test Child Grid View Component</h4>
</div>
<div class="card-body">
<h5>Grid Information:</h5>
<table class="table table-bordered">
<tr>
<td><strong>Grid ID:</strong></td>
<td>@Model.Id</td>
</tr>
<tr>
<td><strong>Grid Name:</strong></td>
<td>@Model.GridName</td>
</tr>
<tr>
<td><strong>View Component Name:</strong></td>
<td>@Model.ViewComponentName</td>
</tr>
<tr>
<td><strong>Level:</strong></td>
<td>@Model.Level</td>
</tr>
<tr>
<td><strong>Parent Grid ID:</strong></td>
<td>@(Model.ParentGridId?.ToString() ?? "None")</td>
</tr>
</table>
@if (Model.DataContext != null && Model.DataContext.Any())
{
<h5 class="mt-3">Data Context:</h5>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Type</th>
</tr>
</thead>
<tbody>
@foreach (var kvp in Model.DataContext)
{
<tr>
<td><strong>@kvp.Key</strong></td>
<td>@kvp.Value</td>
<td><em>@kvp.Value.GetType().Name</em></td>
</tr>
}
</tbody>
</table>
@if (Model.DataContext.ContainsKey("contextId"))
{
<div class="alert alert-success mt-3">
<strong>Context ID Found:</strong> @Model.DataContext["contextId"]
</div>
}
else
{
<div class="alert alert-warning mt-3">
<strong>Warning:</strong> No contextId found in DataContext
</div>
}
}
else
{
<div class="alert alert-warning mt-3">
<strong>No Data Context available</strong>
</div>
}
@if (Model.Configuration != null)
{
<h5 class="mt-3">Configuration:</h5>
<table class="table table-bordered">
<tr>
<td><strong>Show Child Grids As Tabs:</strong></td>
<td>@Model.Configuration.ShowChildGridsAsTabs</td>
</tr>
<tr>
<td><strong>Requires Parent Row Selection:</strong></td>
<td>@Model.Configuration.RequiresParentRowSelection</td>
</tr>
<tr>
<td><strong>Description:</strong></td>
<td>@(Model.Configuration.Description ?? "N/A")</td>
</tr>
</table>
}
@if (Model.ChildGrids != null && Model.ChildGrids.Any())
{
<div class="alert alert-info mt-3">
<strong>Child Grids:</strong> This grid has @Model.ChildGrids.Count child grid(s)
<ul class="mb-0 mt-2">
@foreach (var child in Model.ChildGrids)
{
<li>@child.GridName (Level @child.Level)</li>
}
</ul>
</div>
}
else
{
<div class="alert alert-secondary mt-3">
<strong>No child grids</strong>
</div>
}
<h5 class="mt-3">Full Model JSON:</h5>
<pre class="bg-light p-3" style="max-height: 300px; overflow-y: auto;">@JsonSerializer.Serialize(Model, new JsonSerializerOptions { WriteIndented = true })</pre>
</div>
</div>

View File

@ -0,0 +1,466 @@
@model ProductSearchModel
@using Nop.Core.Domain.Catalog;
@{
//page title
ViewBag.PageTitle = T("Admin.Catalog.Products").Text;
//active menu item (system name)
NopHtml.SetActiveMenuItemSystemName("Products");
}
@{
const string hideSearchBlockAttributeName = "ProductListPage.HideSearchBlock";
var hideSearchBlock = await genericAttributeService.GetAttributeAsync<bool>(await workContext.GetCurrentCustomerAsync(), hideSearchBlockAttributeName);
}
@if (Model.LicenseCheckModel.BlockPages != true)
{
<form asp-controller="Product" asp-action="List" method="post">
<div class="content-header clearfix">
<h1 class="float-left">
Fruitbank @T("Admin.Catalog.Products")
</h1>
<div class="float-right">
<a asp-action="Create" class="btn btn-primary">
<i class="fas fa-square-plus"></i>
@T("Admin.Common.AddNew")
</a>
<a asp-action="BulkEdit" class="btn btn-info">
<i class="fas fa-pen"></i>
@T("Admin.Catalog.Products.BulkEdit")
</a>
<button asp-action="DownloadCatalogPDF" type="submit" name="download-catalog-pdf" class="btn bg-purple">
<i class="far fa-file-pdf"></i>
@T("Admin.Catalog.Products.List.DownloadPDF")
</button>
<div class="btn-group">
<button type="button" class="btn btn-success">
<i class="fas fa-download"></i>
@T("Admin.Common.Export")
</button>
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">&nbsp;</span>
</button>
<ul class="dropdown-menu" role="menu">
<li class="dropdown-item">
<button asp-action="ExportToXml" type="submit" name="exportxml-all">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportxml-selected">
<i class="far fa-file-code"></i>
@T("Admin.Common.ExportToXml.Selected")
</button>
</li>
<li class="dropdown-divider"></li>
<li class="dropdown-item">
<button asp-action="ExportToExcel" type="submit" name="exportexcel-all">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.All")
</button>
</li>
<li class="dropdown-item">
<button type="button" id="exportexcel-selected">
<i class="far fa-file-excel"></i>
@T("Admin.Common.ExportToExcel.Selected")
</button>
</li>
</ul>
</div>
@if (!Model.IsLoggedInAsVendor || Model.AllowVendorsToImportProducts)
{
//a vendor cannot import products
<button type="button" name="importexcel" class="btn bg-olive" data-toggle="modal" data-target="#importexcel-window">
<i class="fas fa-upload"></i>
@T("Admin.Common.Import")
</button>
}
<button type="button" id="delete-selected" class="btn btn-danger">
<i class="far fa-trash-can"></i>
@T("Admin.Common.Delete.Selected")
</button>
<nop-action-confirmation asp-button-id="delete-selected" />
@await Component.InvokeAsync(typeof(AdminWidgetViewComponent), new { widgetZone = AdminWidgetZones.ProductListButtons, additionalData = Model })
</div>
</div>
<section class="content">
<div class="container-fluid">
<div class="form-horizontal">
<div class="cards-group">
<div class="card card-default card-search">
<div class="card-body">
<div class="row search-row @(!hideSearchBlock ? "opened" : "")" data-hideAttribute="@hideSearchBlockAttributeName">
<div class="search-text">@T("Admin.Common.Search")</div>
<div class="icon-search"><i class="fas fa-magnifying-glass" aria-hidden="true"></i></div>
<div class="icon-collapse"><i class="far fa-angle-@(!hideSearchBlock ? "up" : "down")" aria-hidden="true"></i></div>
</div>
<div class="search-body @(hideSearchBlock ? "closed" : "")">
<div class="row">
<div class="col-md-5">
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchProductName" />
</div>
<div class="col-md-8">
<nop-editor asp-for="SearchProductName" />
</div>
</div>
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchCategoryId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchCategoryId" asp-items="Model.AvailableCategories" />
</div>
</div>
<div class="form-group row" @(Model.AvailableCategories.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchIncludeSubCategories" />
</div>
<div class="col-md-8">
<nop-editor asp-for="SearchIncludeSubCategories" />
</div>
</div>
<div class="form-group row" @(Model.AvailableManufacturers.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchManufacturerId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchManufacturerId" asp-items="Model.AvailableManufacturers" />
</div>
</div>
<div class="form-group row" @(Model.AvailableVendors.SelectionIsNotPossible() || Model.IsLoggedInAsVendor ? Html.Raw("style='display: none;'") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchVendorId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchVendorId" asp-items="Model.AvailableVendors" />
</div>
</div>
</div>
<div class="col-md-7">
<div class="form-group row" @(Model.HideStoresList ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchStoreId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchStoreId" asp-items="Model.AvailableStores" />
</div>
</div>
<div class="form-group row" @(Model.AvailableWarehouses.SelectionIsNotPossible() ? Html.Raw("style=\"display:none\"") : null)>
<div class="col-md-4">
<nop-label asp-for="SearchWarehouseId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchWarehouseId" asp-items="Model.AvailableWarehouses" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchProductTypeId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchProductTypeId" asp-items="Model.AvailableProductTypes" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="SearchPublishedId" />
</div>
<div class="col-md-8">
<nop-select asp-for="SearchPublishedId" asp-items="Model.AvailablePublishedOptions" />
</div>
</div>
<div class="form-group row">
<div class="col-md-4">
<nop-label asp-for="GoDirectlyToSku" />
</div>
<div class="col-md-8">
<div class="input-group input-group-short">
<nop-editor asp-for="GoDirectlyToSku" />
<span class="input-group-append">
<button type="submit" id="go-to-product-by-sku" name="go-to-product-by-sku" class="btn btn-info btn-flat">
@T("Admin.Common.Go")
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="text-center col-12">
<button type="button" id="search-products" class="btn btn-primary btn-search">
<i class="fas fa-magnifying-glass"></i>
@T("Admin.Common.Search")
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card card-default">
<div class="card-body">
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Products", Docs.Products + Utm.OnAdmin)" />
@await Html.PartialAsync("Table", new DataTablesModel
{
Name = "products-grid",
UrlRead = new DataUrl("ProductList", "CustomProduct", null),
SearchButtonId = "search-products",
Length = Model.PageSize,
LengthMenu = Model.AvailablePageSizes,
Filters = new List<FilterParameter>
{
new FilterParameter(nameof(Model.SearchProductName)),
new FilterParameter(nameof(Model.SearchCategoryId)),
new FilterParameter(nameof(Model.SearchIncludeSubCategories), typeof(bool)),
new FilterParameter(nameof(Model.SearchManufacturerId)),
new FilterParameter(nameof(Model.SearchStoreId)),
new FilterParameter(nameof(Model.SearchWarehouseId)),
new FilterParameter(nameof(Model.SearchVendorId)),
new FilterParameter(nameof(Model.SearchProductTypeId)),
new FilterParameter(nameof(Model.SearchPublishedId))
},
ColumnCollection = new List<ColumnProperty>
{
new ColumnProperty(nameof(ProductModel.Id))
{
IsMasterCheckBox = true,
Render = new RenderCheckBox("checkbox_products"),
ClassName = NopColumnClassDefaults.CenterAll,
Width = "50"
},
new ColumnProperty(nameof(ProductModel.PictureThumbnailUrl))
{
Title = T("Admin.Catalog.Products.Fields.PictureThumbnailUrl").Text,
Width = "100",
Render = new RenderPicture(width: 100)
},
new ColumnProperty(nameof(ProductModel.Name))
{
Title = T("Admin.Catalog.Products.Fields.Name").Text
},
new ColumnProperty(nameof(ProductModel.Sku))
{
Title = T("Admin.Catalog.Products.Fields.Sku").Text,
Width = "100"
},
new ColumnProperty(nameof(ProductModel.FormattedPrice))
{
Title = T("Admin.Catalog.Products.Fields.Price").Text
},
new ColumnProperty(nameof(ProductModel.StockQuantityStr))
{
Title = T("Admin.Catalog.Products.Fields.StockQuantity").Text
},
new ColumnProperty(nameof(ProductModel.Published))
{
Title = T("Admin.Catalog.Products.Fields.Published").Text,
Width = "80",
ClassName = NopColumnClassDefaults.CenterAll,
Render = new RenderBoolean()
},
new ColumnProperty(nameof(ProductModel.Id))
{
Title = T("Admin.Common.Edit").Text,
Width = "80",
ClassName = NopColumnClassDefaults.Button,
Render = new RenderButtonEdit(new DataUrl("~/Admin/Product/Edit"))
}
}
})
<script>
$(function() {
$('#delete-selected-action-confirmation-submit-button').bind('click', function () {
var postData = {
selectedIds: selectedIds
};
addAntiForgeryToken(postData);
$.ajax({
cache: false,
type: "POST",
url: "@(Url.Action("DeleteSelected", "Product"))",
data: postData,
error: function (jqXHR, textStatus, errorThrown) {
showAlert('deleteSelectedFailed', errorThrown);
},
complete: function (jqXHR, textStatus) {
if (jqXHR.status === 204)
{
showAlert('nothingSelectedAlert', '@T("Admin.Common.Alert.NothingSelected")');
return;
}
updateTable('#products-grid');
}
});
$('#delete-selected-action-confirmation').modal('toggle');
return false;
});
});
</script>
<nop-alert asp-alert-id="deleteSelectedFailed" />
<nop-alert asp-alert-id="nothingSelectedAlert" />
</div>
</div>
</div>
</div>
</div>
</section>
<script>
$(function() {
$("#@Html.IdFor(model => model.GoDirectlyToSku)").keydown(function (event) {
if (event.keyCode === 13) {
$("#go-to-product-by-sku").trigger("click");
return false;
}
});
});
</script>
</form>
}
<script>
$(function() {
var displayModal = @((Model.LicenseCheckModel?.DisplayWarning == true || Model.LicenseCheckModel?.BlockPages == true).ToString().ToLower());
if (displayModal) {
$('#license-window').modal({
backdrop: 'static',
keyboard: false
});
$('#license-window').on('shown.bs.modal', function (event) {
var modalCloseEl = $(this).find('button.close');
var closeTextEl = $('span', modalCloseEl);
var startFrom = 5;
closeTextEl.text(startFrom);
const timer = setInterval(function() {
if (startFrom-- > 0)
closeTextEl.text(startFrom);
}, 1000);
setTimeout(function() {
closeTextEl.html('&times;');
modalCloseEl.on('click', function() {
$('#license-window').modal('hide')
});
clearInterval(timer);
}, startFrom*1000);
});
}
});
</script>
<div id="license-window" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
@Html.Raw(Model.LicenseCheckModel?.WarningText)
</div>
</div>
</div>
@*import products form*@
<div id="importexcel-window" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="importexcel-window-title">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="importexcel-window-title">@T("Admin.Common.ImportFromExcel")</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form asp-controller="Product" asp-action="ImportExcel" method="post" enctype="multipart/form-data">
<div class="form-horizontal">
<div class="modal-body">
<ul class="common-list">
<li>
<em>@T("Admin.Catalog.Products.List.ImportFromExcelTip")</em>
</li>
<li>
<em>@T("Admin.Common.ImportFromExcel.ManyRecordsWarning")</em>
</li>
</ul>
<div class="form-group row">
<div class="col-md-2">
<div class="label-wrapper">
<label class="col-form-label">
@T("Admin.Common.ExcelFile")
</label>
</div>
</div>
<div class="col-md-10">
<input type="file" id="importexcelfile" name="importexcelfile" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
@T("Admin.Common.ImportFromExcel")
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@*export selected (XML). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@
<form asp-controller="Product" asp-action="ExportXmlSelected" method="post" id="export-xml-selected-form">
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
</form>
<script>
$(function() {
$('#exportxml-selected').click(function (e) {
e.preventDefault();
var ids = selectedIds.join(",");
if (!ids) {
$('#exportXmlSelected-info').text("@T("Admin.Products.NoProducts")");
$("#exportXmlSelected").trigger("click");
}
else {
$('#export-xml-selected-form #selectedIds').val(ids);
$('#export-xml-selected-form').submit();
updateTable('#products-grid');
}
return false;
});
});
</script>
<nop-alert asp-alert-id="exportXmlSelected" />
@*export selected (Excel). We don't use GET approach because it's limited to 2K-4K chars and won't work for large number of entities*@
<form asp-controller="Product" asp-action="ExportExcelSelected" method="post" id="export-excel-selected-form">
<input type="hidden" id="selectedIds" name="selectedIds" value="" />
</form>
<script>
$(function() {
$('#exportexcel-selected').click(function (e) {
e.preventDefault();
var ids = selectedIds.join(",");
if (!ids) {
$('#exportExcelSelected-info').text("@T("Admin.Products.NoProducts")");
$("#exportExcelSelected").trigger("click");
}
else {
$('#export-excel-selected-form #selectedIds').val(ids);
$('#export-excel-selected-form').submit();
updateTable('#products-grid');
}
return false;
});
});
</script>
<nop-alert asp-alert-id="exportExcelSelected" />

View File

@ -0,0 +1,55 @@
// File: Plugins/YourCompany.ProductAttributes/Components/ProductAttributesViewComponent.cs
using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Plugin.Misc.FruitBankPlugin.Services;
using Nop.Services.Common;
using Nop.Web.Areas.Admin.Models.Catalog;
using Nop.Web.Areas.Admin.Models.Orders;
using Nop.Web.Framework.Components;
namespace Nop.Plugin.Misc.FruitBankPlugin.Components
{
[ViewComponent(Name = "OrderAttributes")]
public class OrderAttributesViewComponent : NopViewComponent
{
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly IWorkContext _workContext;
private readonly IStoreContext _storeContext;
public OrderAttributesViewComponent(FruitBankAttributeService fruitBankAttributeService, IWorkContext workContext, IStoreContext storeContext)
{
_workContext = workContext;
_storeContext = storeContext;
_fruitBankAttributeService = fruitBankAttributeService;
}
public async Task<IViewComponentResult> InvokeAsync(string widgetZone, object additionalData)
{
if (additionalData is not OrderModel orderModel) return Content("");
var model = new OrderAttributesModel { OrderId = orderModel.Id };
if (model.OrderId > 0)
{
var orderPickupAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, DateTime?>(model.OrderId, nameof(IOrderDto.DateOfReceipt));
var orderMeasurableAttributeValue = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Order, bool>(model.OrderId, nameof(OrderModelExtended.IsMeasurable));
model.IsMeasurable = orderMeasurableAttributeValue;
if(orderPickupAttributeValue.HasValue && orderPickupAttributeValue.Value != DateTime.MinValue)
{
model.DateOfReceipt = orderPickupAttributeValue;
}
else
{
model.DateOfReceipt = null;
}
}
return View("~/Plugins/Misc.FruitBankPlugin/Views/OrderAttributes.cshtml", model);
}
}
}

View File

@ -1,4 +1,5 @@
using AyCode.Core.Extensions;
using FruitBank.Common.Interfaces;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.TagHelpers;
@ -43,6 +44,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
{
private FruitBankDbContext _ctx;
private readonly IOrderMeasurementService _orderMeasurementService;
private readonly IGenericAttributeService _genericAttributeService;
public CustomOrderModelFactory(
FruitBankDbContext ctx,
@ -91,36 +93,37 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
OrderSettings orderSettings,
ShippingSettings shippingSettings,
IUrlRecordService urlRecordService,
TaxSettings taxSettings
) : base(addressSettings,
catalogSettings,
currencySettings,
actionContextAccessor,
addressModelFactory,
addressService,
affiliateService,
baseAdminModelFactory,
countryService,
currencyService,
customerService,
dateTimeHelper,
discountService,
downloadService,
encryptionService,
giftCardService,
localizationService,
measureService,
orderProcessingService,
orderReportService,
orderService,
paymentPluginManager,
paymentService,
pictureService,
priceCalculationService,
priceFormatter,
productAttributeService,
productService,
returnRequestService,
TaxSettings taxSettings,
IGenericAttributeService genericAttributeService
) : base(addressSettings,
catalogSettings,
currencySettings,
actionContextAccessor,
addressModelFactory,
addressService,
affiliateService,
baseAdminModelFactory,
countryService,
currencyService,
customerService,
dateTimeHelper,
discountService,
downloadService,
encryptionService,
giftCardService,
localizationService,
measureService,
orderProcessingService,
orderReportService,
orderService,
paymentPluginManager,
paymentService,
pictureService,
priceCalculationService,
priceFormatter,
productAttributeService,
productService,
returnRequestService,
rewardPointService,
settingService,
shipmentService,
@ -141,6 +144,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
{
_ctx = ctx;
_orderMeasurementService = orderMeasurementService;
_genericAttributeService = genericAttributeService;
}
public override async Task<OrderSearchModel> PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
@ -173,7 +177,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
}
public override async Task<OrderListModel> PrepareOrderListModelAsync(OrderSearchModel searchModel)
=>await base.PrepareOrderListModelAsync(searchModel);
=> await base.PrepareOrderListModelAsync(searchModel);
public async Task<OrderListModelExtended> PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel)
{
@ -188,6 +192,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended);
orderModelExtended.IsMeasurable = orderDtosById[orderModel.Id].IsMeasurable;// await ShouldMarkAsNeedsMeasurementAsync(orderModel);
orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel);
orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel);
Console.WriteLine(orderModelExtended.Id);
extendedRows.Add(orderModelExtended);
@ -216,7 +222,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
return await Task.FromResult(false);
}
private async Task<DateTime?> GetPickupDateTimeAsync(OrderModel order)
{
DateTime? dateTime = DateTime.MinValue;
var fullOrder = await _orderService.GetOrderByIdAsync(order.Id);
if (fullOrder != null)
{
dateTime = await _genericAttributeService.GetAttributeAsync<DateTime>(fullOrder, nameof(IOrderDto.DateOfReceipt));
if(dateTime == DateTime.MinValue || !dateTime.HasValue)
{
dateTime = null;
}
}
return dateTime;
}
}
}

View File

@ -68,7 +68,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
};
await _settingService.SaveSettingAsync(settings);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "EN");
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Shipment", "HU");
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.ShipmentsList", "Szállítmányok", "HU");
await base.InstallAsync();
}
@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
public Task<IList<string>> GetWidgetZonesAsync()
{
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock });
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom, AdminWidgetZones.ProductDetailsBlock, AdminWidgetZones.OrderDetailsBlock });
}
//public string GetWidgetViewComponentName(string widgetZone)
@ -138,6 +138,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
{
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
}
else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
{
return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null;
}
}
return null;

View File

@ -111,6 +111,26 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.Admin.ManagementPage.GetPartners",
pattern: "Admin/ManagementPage/GetPartners",
defaults: new { controller = "ManagementPage", action = "GetPartners", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.ManagementPage.LoadChildGrid",
pattern: "Admin/ManagementPage/LoadChildGrid",
defaults: new { controller = "ManagementPage", action = "LoadChildGrid", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Products.List",
pattern: "Admin/Product/List",
defaults: new { controller = "CustomProduct", action = "List", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Products.ProductList",
pattern: "Admin/Product/ProductList",
defaults: new { controller = "CustomProduct", action = "ProductList", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Admin.Orders.SaveOrderAttributes",
pattern: "Admin/CustomOrder/SaveOrderAttributes",
defaults: new { controller = "CustomOrder", action = "SaveOrderAttributes", area = AreaNames.ADMIN });
}
/// <summary>

View File

@ -0,0 +1,18 @@
using FruitBank.Common.Interfaces;
using Nop.Web.Framework.Models;
using Nop.Web.Framework.Mvc.ModelBinding;
namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{
public record OrderAttributesModel : BaseNopModel, IMeasurable
{
public int OrderId { get; set; }
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.IsMeasurable")]
public bool IsMeasurable { get; set; }
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.DateOfReceipt")]
public DateTime? DateOfReceipt { get; set; }
}
}

View File

@ -5,6 +5,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
public partial record OrderModelExtended : OrderModel
{
public bool IsMeasurable { get; set; }
public DateTime? DateOfReceipt { get; set; }
}
}

View File

@ -7,6 +7,9 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\InnvoiceApiService.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="logo.jpg" />
@ -28,7 +31,10 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="PdfPig" Version="0.1.11" />
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Tesseract" Version="5.2.0" />
<PackageReference Include="TesseractOCR" Version="5.5.1" />
</ItemGroup>
<ItemGroup>
@ -37,6 +43,11 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\Product\List.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\_ViewStart.cshtml">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
@ -146,7 +157,10 @@
</ItemGroup>
<ItemGroup>
<None Update="Areas\Admin\Components\_DocumentsGridPartial.cshtml">
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Order\TestGridComponent.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Order\ShippingDocumentGridComponent.cshtml">
@ -605,6 +619,9 @@
<None Update="Views\ProductAIListWidget.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\OrderAttributes.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\ProductAttributes.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
@ -620,6 +637,10 @@
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<None Include="Services\InnvoiceApiService.cs" />
</ItemGroup>
<!-- This target execute after "Build" target -->
<Target Name="NopTarget" AfterTargets="Build">
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />

View File

@ -1,14 +1,6 @@
using Azure;
using Nop.Core;
using Nop.Core;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Stores;
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
@ -37,16 +29,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return response;
}
//public async Task<string> GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
public async Task<string> GetOpenAIPDFAnalysisFromText(string pdfText, string userQuestion)
{
string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
@ -57,15 +39,5 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return fixedResponse;
}
//public async Task<string> GetOpenAIPartnerInfoFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
}
}

View File

@ -0,0 +1,496 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using System.Linq;
using System.Text;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
{
/// <summary>
/// Service for interacting with InnVoice Invoice API
/// API Documentation: https://help.innvoice.hu/hc/hu/articles/360003142839
/// </summary>
public class InnVoiceApiService
{
private readonly HttpClient _httpClient;
private readonly string _companyName;
private readonly string _username;
private readonly string _password;
private readonly string _baseUrl;
public InnVoiceApiService(string companyName, string username, string password, string baseUrl = "https://api.innvoice.hu")
{
_companyName = companyName ?? throw new ArgumentNullException(nameof(companyName));
_username = username ?? throw new ArgumentNullException(nameof(username));
_password = password ?? throw new ArgumentNullException(nameof(password));
_baseUrl = baseUrl.TrimEnd('/');
_httpClient = new HttpClient();
SetupAuthentication();
}
private void SetupAuthentication()
{
var authToken = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{_username}:{_password}")
);
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
}
/// <summary>
/// Get all invoices
/// Rate limit: 20 times per hour without ID parameter
/// </summary>
public async Task<List<Invoice>> GetAllInvoicesAsync()
{
var url = $"{_baseUrl}/{_companyName}/invoice";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoice by internal table ID
/// </summary>
public async Task<Invoice> GetInvoiceByIdAsync(int tableId)
{
var url = $"{_baseUrl}/{_companyName}/invoice/id/{tableId}";
var invoices = await GetInvoicesFromUrlAsync(url);
return invoices.Count > 0 ? invoices[0] : null;
}
/// <summary>
/// Get invoice by invoice number
/// </summary>
public async Task<List<Invoice>> GetInvoiceByNumberAsync(string invoiceNumber)
{
var url = $"{_baseUrl}/{_companyName}/invoice/invoicenumber/{Uri.EscapeDataString(invoiceNumber)}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by creation date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByCreationDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/created/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by fulfillment date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByFulfillmentDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/fulfillment/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by due date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByDueDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/duedate/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by payment date
/// Format: YYYYMMDD
/// </summary>
public async Task<List<Invoice>> GetInvoicesByPaymentDateAsync(DateTime date)
{
var dateStr = date.ToString("yyyyMMdd");
var url = $"{_baseUrl}/{_companyName}/invoice/paymentdate/{dateStr}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices by customer tax number
/// </summary>
public async Task<List<Invoice>> GetInvoicesByCustomerTaxNumberAsync(string taxNumber)
{
var url = $"{_baseUrl}/{_companyName}/invoice/taxnumber/{Uri.EscapeDataString(taxNumber)}";
return await GetInvoicesFromUrlAsync(url);
}
/// <summary>
/// Get invoices modified since a specific timestamp
/// Format: YYYYMMDDHHmmss (year, month, day, hour, minute, second)
/// Recommended for tracking changes every 10 minutes
/// Rate limit: Full queries or queries older than current month limited to 10 times per 30 days
/// Recommended: Only use current month dates
/// </summary>
public async Task<List<Invoice>> GetInvoicesByUpdateTimeAsync(DateTime updateTime)
{
var timeStr = updateTime.ToString("yyyyMMddHHmmss");
var url = $"{_baseUrl}/{_companyName}/invoice/updatedtime/{timeStr}";
return await GetInvoicesFromUrlAsync(url);
}
private async Task<List<Invoice>> GetInvoicesFromUrlAsync(string url)
{
try
{
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var invoices = JsonSerializer.Deserialize<List<Invoice>>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return invoices ?? new List<Invoice>();
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error calling InnVoice API: {ex.Message}", ex);
}
catch (JsonException ex)
{
throw new InnVoiceApiException($"Error parsing API response: {ex.Message}", ex);
}
}
}
// Models
public class Invoice
{
[JsonPropertyName("TABLE_ID")]
public int TableId { get; set; }
[JsonPropertyName("InvoiceNumber")]
public string InvoiceNumber { get; set; }
[JsonPropertyName("Created")]
public string Created { get; set; }
[JsonPropertyName("Fulfillment")]
public string Fulfillment { get; set; }
[JsonPropertyName("DueDate")]
public string DueDate { get; set; }
[JsonPropertyName("PaymentDate")]
public string PaymentDate { get; set; }
[JsonPropertyName("CustomerName")]
public string CustomerName { get; set; }
[JsonPropertyName("CustomerTaxNumber")]
public string CustomerTaxNumber { get; set; }
[JsonPropertyName("CustomerAddress")]
public string CustomerAddress { get; set; }
[JsonPropertyName("TotalNet")]
public decimal TotalNet { get; set; }
[JsonPropertyName("TotalGross")]
public decimal TotalGross { get; set; }
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Status")]
public string Status { get; set; }
[JsonPropertyName("InvoiceType")]
public string InvoiceType { get; set; }
[JsonPropertyName("PaymentMethod")]
public string PaymentMethod { get; set; }
// Add more properties as needed based on actual API response
}
public class InnVoiceApiException : Exception
{
public InnVoiceApiException(string message) : base(message) { }
public InnVoiceApiException(string message, Exception innerException) : base(message, innerException) { }
}
// Invoice Creation Models
public class InvoiceCreateRequest
{
public int VevoID { get; set; } = 0;
public string VevoNev { get; set; }
public string VevoIrsz { get; set; }
public string VevoOrszag { get; set; }
public string VevoTelep { get; set; }
public string VevoUtcaHsz { get; set; }
public string VevoEPNev { get; set; }
public string VevoEPKod { get; set; }
public string SzallNev { get; set; }
public string SzallIrsz { get; set; }
public string SzallTelep { get; set; }
public string SzallUtcaHsz { get; set; }
public string SzallOrszag { get; set; }
public int SzamlatombID { get; set; }
public DateTime SzamlaKelte { get; set; }
public DateTime TeljesitesKelte { get; set; }
public DateTime Hatarido { get; set; }
public string Devizanem { get; set; }
public string FizetesiMod { get; set; }
public string Megjegyzes { get; set; }
public string Nyelv1 { get; set; }
public string Nyelv2 { get; set; }
public decimal? Arfolyam { get; set; }
public string ArfolyamDeviza { get; set; }
public bool Fizetve { get; set; }
public bool Eszamla { get; set; }
public string VevoAdoszam { get; set; }
public string VevoCsAdoszam { get; set; }
public string Telefon { get; set; }
public string Email { get; set; }
public string MegrendelesSzamStr { get; set; }
public string MegrendelesIdopontStr { get; set; }
public bool Felretett { get; set; }
public bool Proforma { get; set; }
public bool AutomatikusAr { get; set; }
public bool Eloleg { get; set; }
public bool Sendmail { get; set; }
public string MailSubject { get; set; }
public string MailBody { get; set; }
public string Eredetiszamla { get; set; }
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
public void AddItem(InvoiceItem item)
{
Items.Add(item);
}
public string ToXml()
{
var invoices = new XElement("invoices");
var invoice = new XElement("invoice");
if (VevoID > 0)
invoice.Add(new XElement("VevoID", new XCData(VevoID.ToString())));
invoice.Add(new XElement("VevoNev", new XCData(VevoNev ?? "")));
invoice.Add(new XElement("VevoIrsz", new XCData(VevoIrsz ?? "")));
invoice.Add(new XElement("VevoTelep", new XCData(VevoTelep ?? "")));
invoice.Add(new XElement("VevoOrszag", new XCData(VevoOrszag ?? "")));
invoice.Add(new XElement("VevoUtcaHsz", new XCData(VevoUtcaHsz ?? "")));
if (!string.IsNullOrEmpty(VevoEPNev))
invoice.Add(new XElement("VevoEPNev", new XCData(VevoEPNev)));
if (!string.IsNullOrEmpty(VevoEPKod))
invoice.Add(new XElement("VevoEPKod", new XCData(VevoEPKod)));
if (!string.IsNullOrEmpty(SzallNev))
invoice.Add(new XElement("SzallNev", new XCData(SzallNev)));
if (!string.IsNullOrEmpty(SzallIrsz))
invoice.Add(new XElement("SzallIrsz", new XCData(SzallIrsz)));
if (!string.IsNullOrEmpty(SzallTelep))
invoice.Add(new XElement("SzallTelep", new XCData(SzallTelep)));
if (!string.IsNullOrEmpty(SzallUtcaHsz))
invoice.Add(new XElement("SzallUtcaHsz", new XCData(SzallUtcaHsz)));
if (!string.IsNullOrEmpty(SzallOrszag))
invoice.Add(new XElement("SzallOrszag", new XCData(SzallOrszag)));
invoice.Add(new XElement("SzamlatombID", new XCData(SzamlatombID.ToString())));
invoice.Add(new XElement("SzamlaKelte", new XCData(SzamlaKelte.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("TeljesitesKelte", new XCData(TeljesitesKelte.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("Hatarido", new XCData(Hatarido.ToString("yyyy.MM.dd."))));
invoice.Add(new XElement("Devizanem", new XCData(Devizanem ?? "")));
invoice.Add(new XElement("FizetesiMod", new XCData(FizetesiMod ?? "")));
if (!string.IsNullOrEmpty(Megjegyzes))
invoice.Add(new XElement("Megjegyzes", new XCData(Megjegyzes)));
if (!string.IsNullOrEmpty(Nyelv1))
invoice.Add(new XElement("Nyelv1", new XCData(Nyelv1)));
if (!string.IsNullOrEmpty(Nyelv2))
invoice.Add(new XElement("Nyelv2", new XCData(Nyelv2)));
if (Arfolyam.HasValue)
invoice.Add(new XElement("Arfolyam", new XCData(Arfolyam.Value.ToString())));
if (!string.IsNullOrEmpty(ArfolyamDeviza))
invoice.Add(new XElement("ArfolyamDeviza", new XCData(ArfolyamDeviza)));
invoice.Add(new XElement("Fizetve", Fizetve ? "1" : "0"));
invoice.Add(new XElement("Eszamla", Eszamla ? "1" : "0"));
if (!string.IsNullOrEmpty(VevoAdoszam))
invoice.Add(new XElement("VevoAdoszam", new XCData(VevoAdoszam)));
if (!string.IsNullOrEmpty(VevoCsAdoszam))
invoice.Add(new XElement("VevoCsAdoszam", new XCData(VevoCsAdoszam)));
if (!string.IsNullOrEmpty(Telefon))
invoice.Add(new XElement("Telefon", new XCData(Telefon)));
if (!string.IsNullOrEmpty(Email))
invoice.Add(new XElement("Email", new XCData(Email)));
if (!string.IsNullOrEmpty(MegrendelesSzamStr))
invoice.Add(new XElement("MegrendelesSzamStr", new XCData(MegrendelesSzamStr)));
if (!string.IsNullOrEmpty(MegrendelesIdopontStr))
invoice.Add(new XElement("MegrendelesIdopontStr", new XCData(MegrendelesIdopontStr)));
invoice.Add(new XElement("Felretett", Felretett ? "1" : "0"));
invoice.Add(new XElement("Proforma", Proforma ? "1" : "0"));
if (AutomatikusAr)
invoice.Add(new XElement("AutomatikusAr", "1"));
if (Eloleg)
invoice.Add(new XElement("Eloleg", "1"));
if (Sendmail)
{
invoice.Add(new XElement("Sendmail", "1"));
if (!string.IsNullOrEmpty(MailSubject))
invoice.Add(new XElement("MailSubject", new XCData(MailSubject)));
if (!string.IsNullOrEmpty(MailBody))
invoice.Add(new XElement("MailBody", new XCData(MailBody)));
}
if (!string.IsNullOrEmpty(Eredetiszamla))
invoice.Add(new XElement("Eredetiszamla", new XCData(Eredetiszamla)));
// Add items
foreach (var item in Items)
{
var tetel = new XElement("tetel");
tetel.Add(new XElement("TetelNev", new XCData(item.TetelNev ?? "")));
tetel.Add(new XElement("AfaSzoveg", item.AfaSzoveg ?? ""));
tetel.Add(new XElement("Brutto", item.Brutto ? "1" : "0"));
tetel.Add(new XElement("EgysegAr", item.EgysegAr.ToString()));
tetel.Add(new XElement("Mennyiseg", item.Mennyiseg.ToString()));
tetel.Add(new XElement("MennyisegEgyseg", new XCData(item.MennyisegEgyseg ?? "")));
if (item.KedvezmenyOsszeg.HasValue)
tetel.Add(new XElement("KedvezmenyOsszeg", item.KedvezmenyOsszeg.Value.ToString()));
if (item.TermekID.HasValue)
tetel.Add(new XElement("TermekID", item.TermekID.Value.ToString()));
if (!string.IsNullOrEmpty(item.Megjegyzes))
tetel.Add(new XElement("Megjegyzes", new XCData(item.Megjegyzes)));
if (!string.IsNullOrEmpty(item.CikkSzam))
tetel.Add(new XElement("CikkSzam", new XCData(item.CikkSzam)));
if (!string.IsNullOrEmpty(item.VTSZSZJ))
tetel.Add(new XElement("VTSZSZJ", new XCData(item.VTSZSZJ)));
if (item.ElolegSzamlaTABLE_ID.HasValue)
tetel.Add(new XElement("ElolegSzamlaTABLE_ID", item.ElolegSzamlaTABLE_ID.Value.ToString()));
if (!string.IsNullOrEmpty(item.ElolegSzamlaSorszam))
tetel.Add(new XElement("ElolegSzamlaSorszam", new XCData(item.ElolegSzamlaSorszam)));
invoice.Add(tetel);
}
invoices.Add(invoice);
return new XDeclaration("1.0", "UTF-8", null).ToString() + "\n" + invoices.ToString();
}
}
public class InvoiceItem
{
public string TetelNev { get; set; }
public string AfaSzoveg { get; set; }
public bool Brutto { get; set; }
public decimal EgysegAr { get; set; }
public decimal Mennyiseg { get; set; }
public string MennyisegEgyseg { get; set; }
public decimal? KedvezmenyOsszeg { get; set; }
public int? TermekID { get; set; }
public string Megjegyzes { get; set; }
public string CikkSzam { get; set; }
public string VTSZSZJ { get; set; }
public int? ElolegSzamlaTABLE_ID { get; set; }
public string ElolegSzamlaSorszam { get; set; }
}
public class InvoiceCreateResponse
{
public string ErrorCode { get; set; }
public string Message { get; set; }
public int? TableId { get; set; }
public int? VevoID { get; set; }
public string TechId { get; set; }
public string Sorszam { get; set; }
public string PrintUrl { get; set; }
public bool IsSuccess => ErrorCode == "200";
public static InvoiceCreateResponse FromXml(string xml)
{
var doc = XDocument.Parse(xml);
var invoice = doc.Descendants("invoice").FirstOrDefault();
if (invoice == null)
{
throw new InnVoiceApiException("Invalid XML response format");
}
return new InvoiceCreateResponse
{
ErrorCode = invoice.Element("error")?.Value,
Message = invoice.Element("message")?.Value?.Trim(),
TableId = int.TryParse(invoice.Element("TABLE_ID")?.Value?.Trim(), out var tid) ? tid : (int?)null,
VevoID = int.TryParse(invoice.Element("VevoID")?.Value?.Trim(), out var vid) ? vid : (int?)null,
TechId = invoice.Element("techid")?.Value?.Trim(),
Sorszam = invoice.Element("Sorszam")?.Value?.Trim(),
PrintUrl = invoice.Element("PrintUrl")?.Value?.Trim()
};
}
}
/// <summary>
/// Create a new invoice
/// </summary>
public async Task<InvoiceCreateResponse> CreateInvoiceAsync(InvoiceCreateRequest request)
{
var url = $"{_baseUrl}/{_companyName}/invoice";
var xml = request.ToXml();
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("data", xml)
});
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return InvoiceCreateResponse.FromXml(responseContent);
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error creating invoice: {ex.Message}", ex);
}
}
/// <summary>
/// Update an existing invoice
/// </summary>
public async Task<InvoiceCreateResponse> UpdateInvoiceAsync(int tableId, InvoiceCreateRequest request)
{
// Set the VevoID if updating customer information
var url = $"{_baseUrl}/{_companyName}/invoice";
var xml = request.ToXml();
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("data", xml),
new KeyValuePair<string, string>("id", tableId.ToString())
});
try
{
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
return InvoiceCreateResponse.FromXml(responseContent);
}
catch (HttpRequestException ex)
{
throw new InnVoiceApiException($"Error updating invoice: {ex.Message}", ex);
}
}

View File

@ -21,6 +21,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
private const string BaseUrl = "https://api.openai.com/v1";
private string? _assistantId;
private string? _vectorStoreId;
public OpenAIApiService(ISettingService settingService, HttpClient httpClient)
{
@ -286,66 +290,371 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
#endregion
#region === PDF ANALYSIS (NEW) ===
public async Task<string?> AnalyzePdfAsync(string filePath, string userPrompt)
{
// Step 1: Upload PDF
using var form = new MultipartFormDataContent();
using var fileStream = File.OpenRead(filePath);
var fileContent = new StreamContent(fileStream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(fileContent, "file", Path.GetFileName(filePath));
form.Add(new StringContent("assistants"), "purpose");
var uploadResponse = await _httpClient.PostAsync(OpenAiFileEndpoint, form);
if (!uploadResponse.IsSuccessStatusCode)
private async Task EnsureAssistantAndVectorStoreAsync()
{
// Find or create vector store
if (_vectorStoreId == null)
{
var error = await uploadResponse.Content.ReadAsStringAsync();
throw new Exception($"File upload failed: {error}");
_vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store");
}
using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync());
var fileId = uploadJson.RootElement.GetProperty("id").GetString();
// Step 2: Ask model with file reference
var requestBody = new
// Find or create assistant
if (_assistantId == null)
{
model = "gpt-4.1", // must support file_search
messages = new[]
_assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant");
}
}
//TEMPORARY: Cleanup all assistants (for testing purposes) - A.
public async Task CleanupAllAssistantsAsync()
{
Console.WriteLine("Cleaning up all existing assistants...");
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var assistants = json.RootElement.GetProperty("data");
foreach (var assistant in assistants.EnumerateArray())
{
new { role = "system", content = "You are an assistant that analyzes uploaded PDF files." },
new { role = "user", content = userPrompt }
},
tools = new[]
var id = assistant.GetProperty("id").GetString();
var name = assistant.GetProperty("name").GetString();
var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/assistants/{id}");
deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
await _httpClient.SendAsync(deleteRequest);
Console.WriteLine($"Deleted assistant: {name} ({id})");
}
Console.WriteLine("Cleanup complete!");
}
// Reset local cache
_assistantId = null;
}
public async Task<string?> AnalyzePdfAsync(Stream file, string fileName, string userPrompt)
{
await EnsureAssistantAndVectorStoreAsync();
var fileId = await UploadFileAsync(file, fileName);
var isImage = IsImageFile(fileName);
if (!isImage)
{
await AttachFileToVectorStoreAsync(fileId);
}
var threadId = await CreateThreadAsync();
if (isImage)
{
await AddUserMessageWithImageAsync(threadId, userPrompt, fileId);
}
else
{
await AddUserMessageAsync(threadId, userPrompt);
}
var runId = await CreateRunAsync(threadId);
await WaitForRunCompletionAsync(threadId, runId);
return await GetAssistantResponseAsync(threadId);
}
private bool IsImageFile(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".gif" || extension == ".webp";
}
private async Task<string> UploadFileAsync(Stream file, string fileName)
{
using var form = new MultipartFormDataContent();
var fileContent = new StreamContent(file);
// Determine MIME type based on file extension
var extension = Path.GetExtension(fileName).ToLowerInvariant();
fileContent.Headers.ContentType = extension switch
{
".pdf" => new MediaTypeHeaderValue("application/pdf"),
".jpg" or ".jpeg" => new MediaTypeHeaderValue("image/jpeg"),
".png" => new MediaTypeHeaderValue("image/png"),
".gif" => new MediaTypeHeaderValue("image/gif"),
".webp" => new MediaTypeHeaderValue("image/webp"),
_ => new MediaTypeHeaderValue("application/octet-stream")
};
form.Add(fileContent, "file", fileName);
form.Add(new StringContent("assistants"), "purpose");
var response = await _httpClient.PostAsync($"{BaseUrl}/files", form);
await EnsureSuccessAsync(response, "upload file");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
private async Task AttachFileToVectorStoreAsync(string fileId)
{
var body = new { file_id = fileId };
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/vector_stores/{_vectorStoreId}/files",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "attach file to vector store");
}
private async Task<string> CreateThreadAsync()
{
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads",
new { }
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "create thread");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
private async Task AddUserMessageAsync(string threadId, string userPrompt)
{
var body = new
{
role = "user",
content = userPrompt
};
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/messages",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "add user message");
}
private async Task AddUserMessageWithImageAsync(string threadId, string userPrompt, string fileId)
{
var body = new
{
role = "user",
content = new object[]
{
new { type = "file_search" }
},
new { type = "text", text = userPrompt },
new { type = "image_file", image_file = new { file_id = fileId } }
}
};
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/messages",
body
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "add user message with image");
}
private async Task<string> CreateRunAsync(string threadId)
{
var body = new
{
assistant_id = _assistantId,
tool_resources = new
{
file_search = new
{
vector_store_ids = new string[] { fileId! }
vector_store_ids = new[] { _vectorStoreId }
}
}
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var request = CreateAssistantRequest(
HttpMethod.Post,
$"{BaseUrl}/threads/{threadId}/runs",
body
);
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
chatResponse.EnsureSuccessStatusCode();
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "create run");
using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync());
var result = responseJson.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
return result ?? "No response from model";
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
return json.RootElement.GetProperty("id").GetString()!;
}
#endregion
private async Task WaitForRunCompletionAsync(string threadId, string runId)
{
const int pollIntervalMs = 1000;
const int maxAttempts = 60; // 1 minute timeout
int attempts = 0;
while (attempts < maxAttempts)
{
var request = CreateAssistantRequest(
HttpMethod.Get,
$"{BaseUrl}/threads/{threadId}/runs/{runId}"
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "check run status");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var status = json.RootElement.GetProperty("status").GetString()!;
if (status == "completed")
return;
if (status != "in_progress" && status != "queued")
throw new Exception($"Run failed with status: {status}");
await Task.Delay(pollIntervalMs);
attempts++;
}
throw new TimeoutException("Run did not complete within the expected time");
}
private async Task<string?> GetAssistantResponseAsync(string threadId)
{
var request = CreateAssistantRequest(
HttpMethod.Get,
$"{BaseUrl}/threads/{threadId}/messages"
);
var response = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(response, "retrieve messages");
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var messages = json.RootElement.GetProperty("data");
if (messages.GetArrayLength() == 0)
return "No response";
var firstMessage = messages[0]
.GetProperty("content")[0]
.GetProperty("text")
.GetProperty("value")
.GetString();
return firstMessage ?? "No response";
}
private HttpRequestMessage CreateAssistantRequest(HttpMethod method, string url, object? body = null)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add("OpenAI-Beta", "assistants=v2");
if (body != null)
{
var json = JsonSerializer.Serialize(body, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
return request;
}
private async Task EnsureSuccessAsync(HttpResponseMessage response, string operation)
{
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Error Status: {response.StatusCode}");
Console.WriteLine($"Error Body: {errorBody}");
throw new Exception($"Failed to {operation}: {errorBody}");
}
}
private async Task<string> FindOrCreateVectorStoreAsync(string name)
{
// List existing vector stores
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var stores = json.RootElement.GetProperty("data");
foreach (var store in stores.EnumerateArray())
{
if (store.GetProperty("name").GetString() == name)
{
return store.GetProperty("id").GetString()!;
}
}
}
// Create new if not found
var createBody = new { name = name };
var createRequest = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/vector_stores", createBody);
var createResponse = await _httpClient.SendAsync(createRequest);
await EnsureSuccessAsync(createResponse, "create vector store");
using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
return createJson.RootElement.GetProperty("id").GetString()!;
}
private async Task<string> FindOrCreateAssistantAsync(string name)
{
// List existing assistants
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/assistants");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var assistants = json.RootElement.GetProperty("data");
foreach (var assistant in assistants.EnumerateArray())
{
if (assistant.GetProperty("name").GetString() == name)
{
return assistant.GetProperty("id").GetString()!;
}
}
}
// Create new if not found
var assistantBody = new
{
name = name,
instructions = "You are an assistant that analyzes uploaded files. When you receive an image, analyze and describe what you see in the image in detail. When you receive a PDF or text document, use the file_search tool to find and analyze relevant information. Always respond directly to the user's question about the file they uploaded.",
model = "gpt-4o",
tools = new[] { new { type = "file_search" } }
};
var request = CreateAssistantRequest(HttpMethod.Post, $"{BaseUrl}/assistants", assistantBody);
var createResponse = await _httpClient.SendAsync(request);
await EnsureSuccessAsync(createResponse, "create assistant");
using var createJson = await JsonDocument.ParseAsync(await createResponse.Content.ReadAsStreamAsync());
return createJson.RootElement.GetProperty("id").GetString()!;
}
#endregion
}
}

View File

@ -0,0 +1,61 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Models.OrderAttributesModel
<div class="card card-default">
<div class="card-header">
<i class="fas fa-tags"></i>
Custom Order Attributes
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="IsMeasurable" />
</div>
<div class="col-md-9">
<nop-editor asp-for="IsMeasurable" />
<span asp-validation-for="IsMeasurable"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-3">
<nop-label asp-for="DateOfReceipt" />
</div>
<div class="col-md-9">
<nop-editor asp-for="DateOfReceipt" />
<span asp-validation-for="DateOfReceipt"></span>
</div>
</div>
<div class="form-group row">
<div class="col-md-12 text-right">
<button type="button" id="saveAttributesBtn" class="btn btn-primary">
<i class="fa fa-save"></i> Save Attributes
</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$("#saveAttributesBtn").click(function () {
$.ajax({
type: "POST",
url: "@Url.Action("SaveOrderAttributes", "CustomOrder")",
data: {
orderId: "@Model.OrderId",
isMeasurable: $("#@Html.IdFor(m => m.IsMeasurable)").is(":checked"),
pickupDateTimeUtc: $("#@Html.IdFor(m => m.DateOfReceipt)").val(),
__RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val()
},
success: function () {
alert("Attributes saved successfully");
},
error: function () {
alert("Error saving attributes");
}
});
});
});
</script>

View File

@ -1,4 +1,4 @@
@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@

@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel