ReceiptDate, PDF extraction by AI file-search, image text extraction by GPT vision, fixes, extra bugs
This commit is contained in:
parent
7f53fccc4f
commit
959cbf5d62
|
|
@ -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);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
@ -8,6 +8,8 @@ using Nop.Core.Domain.Orders;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Factories;
|
using Nop.Plugin.Misc.FruitBankPlugin.Factories;
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
using Nop.Plugin.Misc.FruitBankPlugin.Models;
|
||||||
|
using Nop.Services.Common;
|
||||||
|
using Nop.Services.Messages;
|
||||||
using Nop.Services.Orders;
|
using Nop.Services.Orders;
|
||||||
using Nop.Services.Security;
|
using Nop.Services.Security;
|
||||||
using Nop.Web.Areas.Admin.Controllers;
|
using Nop.Web.Areas.Admin.Controllers;
|
||||||
|
|
@ -26,14 +28,18 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
private readonly CustomOrderModelFactory _orderModelFactory;
|
private readonly CustomOrderModelFactory _orderModelFactory;
|
||||||
private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint;
|
private readonly ICustomOrderSignalREndpointServer _customOrderSignalREndpoint;
|
||||||
private readonly IPermissionService _permissionService;
|
private readonly IPermissionService _permissionService;
|
||||||
|
private readonly IGenericAttributeService _genericAttributeService;
|
||||||
|
private readonly INotificationService _notificationService;
|
||||||
// ... other dependencies
|
// ... 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;
|
_orderService = orderService;
|
||||||
_orderModelFactory = orderModelFactory as CustomOrderModelFactory;
|
_orderModelFactory = orderModelFactory as CustomOrderModelFactory;
|
||||||
_customOrderSignalREndpoint = customOrderSignalREndpoint;
|
_customOrderSignalREndpoint = customOrderSignalREndpoint;
|
||||||
_permissionService = permissionService;
|
_permissionService = permissionService;
|
||||||
|
_genericAttributeService = genericAttributeService;
|
||||||
|
_notificationService = notificationService;
|
||||||
// ... initialize other deps
|
// ... initialize other deps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +107,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
|
|
||||||
// return Json(model);
|
// 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 });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
private readonly IPermissionService _permissionService;
|
private readonly IPermissionService _permissionService;
|
||||||
protected readonly FruitBankDbContext _dbContext;
|
protected readonly FruitBankDbContext _dbContext;
|
||||||
protected readonly AICalculationService _aiCalculationService;
|
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;
|
_permissionService = permissionService;
|
||||||
_dbContext = fruitBankDbContext;
|
_dbContext = fruitBankDbContext;
|
||||||
_aiCalculationService = aiCalculationService;
|
_aiCalculationService = aiCalculationService;
|
||||||
|
_openAIApiService = openAIApiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Test()
|
public async Task<IActionResult> Test()
|
||||||
|
|
@ -190,6 +192,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
|
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
|
||||||
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
|
public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
var shippingDocument = await _dbContext.ShippingDocuments.GetByIdAsync(shippingDocumentId);
|
||||||
//checks
|
//checks
|
||||||
// - files exist
|
// - files exist
|
||||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||||
|
|
@ -209,8 +213,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
var filesList = new List<Files>();
|
var filesList = new List<Files>();
|
||||||
var shippingDocumentToFileList = new List<ShippingDocumentToFiles>();
|
|
||||||
|
|
||||||
|
|
||||||
//iteratation 1: iterate documents to determine their type by AI
|
//iteratation 1: iterate documents to determine their type by AI
|
||||||
|
|
||||||
|
|
@ -218,16 +221,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
var fileName = file.FileName;
|
var fileName = file.FileName;
|
||||||
var fileSize = file.Length;
|
var fileSize = file.Length;
|
||||||
var dbFile = new Files();
|
var dbFile = new Files();
|
||||||
string pdfText = "";
|
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))
|
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase) && !file.ContentType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
return Json(new { success = false, errorMessage = "Only PDF files are allowed" });
|
return Json(new { success = false, errorMessage = "Only PDF or jpg files are allowed" });
|
||||||
|
|
||||||
// Validate file size (max 20MB)
|
// Validate file size (max 20MB)
|
||||||
if (file.Length > 20 * 1024 * 1024)
|
if (file.Length > 20 * 1024 * 1024)
|
||||||
|
|
@ -235,36 +237,89 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
|
|
||||||
// - get text extracted from pdf
|
// - get text extracted from pdf
|
||||||
// Validate file type (PDF only)
|
// 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)
|
||||||
{
|
{
|
||||||
|
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){
|
||||||
try
|
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))
|
|
||||||
{
|
{
|
||||||
// Now you can analyze the PDF content
|
// Open the PDF from the IFormFile's stream directly in memory
|
||||||
|
using (var stream = file.OpenReadStream())
|
||||||
foreach (var page in pdf.GetPages())
|
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream))
|
||||||
{
|
{
|
||||||
// Extract text from each page
|
// Now you can analyze the PDF content
|
||||||
pdfText += ContentOrderTextExtractor.GetText(page);
|
|
||||||
|
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
|
catch (Exception ex)
|
||||||
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
|
{
|
||||||
|
// 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
|
try
|
||||||
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
|
{
|
||||||
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
|
// 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 " +
|
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
|
||||||
"document IN ENGLISH 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)}" +
|
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" +
|
||||||
|
|
@ -282,7 +337,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||||
dbFile.FileName = extractedMetaData.DocumentNumber;
|
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
|
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
|
||||||
// (check if all 3 refers to the same partner)
|
// (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 }
|
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
|
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.TestGridModel
|
||||||
@using DevExtreme.AspNet.Mvc
|
@using DevExtreme.AspNet.Mvc
|
||||||
|
|
||||||
@{
|
|
||||||
|
|
||||||
|
|
||||||
if (Model.DataContext.ContainsKey("contextId"))
|
|
||||||
{
|
|
||||||
|
|
||||||
<text>
|
|
||||||
<h4>@Model.DataContext["contextId"]</h4>
|
|
||||||
</text>
|
|
||||||
}
|
|
||||||
// var gridId = $"dataGrid_{Guid.NewGuid():N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var contextId = Model.DataContext["contextId"];
|
var contextId = Model.DataContext["contextId"];
|
||||||
|
|
@ -21,7 +9,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
<p> @Model.DataContext["contextId"], @Model.GridName, @Model.ViewComponentName </p>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
@model OrderSearchModel
|
@model OrderSearchModel
|
||||||
|
|
||||||
@inject IStoreService storeService
|
@inject IStoreService storeService
|
||||||
|
@using FruitBank.Common.Interfaces
|
||||||
@using Nop.Plugin.Misc.FruitBankPlugin.Models
|
@using Nop.Plugin.Misc.FruitBankPlugin.Models
|
||||||
@using Nop.Services.Stores
|
@using Nop.Services.Stores
|
||||||
@using Nop.Web.Areas.Admin.Components
|
@using Nop.Web.Areas.Admin.Components
|
||||||
|
|
@ -290,9 +291,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card card-default">
|
<div class="card card-default">
|
||||||
<div class="card-header">
|
|
||||||
<h1>RTTTTTTTTTT</h1>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<nop-doc-reference asp-string-resource="@T("Admin.Documentation.Reference.Orders", Docs.Orders + Utm.OnAdmin)" />
|
<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))
|
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
|
||||||
{
|
{
|
||||||
Title = "Needs Measurement",
|
Title = T("Admin.Orders.Fields.ToBeMeasured").Text,
|
||||||
Width = "100",
|
Width = "100",
|
||||||
Render = new RenderCustom("renderColumnIsMeasurable"),
|
Render = new RenderCustom("renderColumnIsMeasurable"),
|
||||||
ClassName = NopColumnClassDefaults.CenterAll
|
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
|
//a vendor does not have access to this functionality
|
||||||
if (!Model.IsLoggedInAsVendor)
|
if (!Model.IsLoggedInAsVendor)
|
||||||
|
|
@ -443,9 +449,14 @@
|
||||||
|
|
||||||
function renderColumnIsMeasurable(data, type, row, meta) {
|
function renderColumnIsMeasurable(data, type, row, meta) {
|
||||||
if(data === true) {
|
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() {
|
$(function() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
||||||
|
|
@ -41,6 +42,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
public class CustomOrderModelFactory : OrderModelFactory
|
public class CustomOrderModelFactory : OrderModelFactory
|
||||||
{
|
{
|
||||||
private readonly IOrderMeasurementService _orderMeasurementService;
|
private readonly IOrderMeasurementService _orderMeasurementService;
|
||||||
|
private readonly IGenericAttributeService _genericAttributeService;
|
||||||
|
|
||||||
public CustomOrderModelFactory(
|
public CustomOrderModelFactory(
|
||||||
IOrderMeasurementService orderMeasurementService,
|
IOrderMeasurementService orderMeasurementService,
|
||||||
|
|
@ -88,36 +90,37 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
OrderSettings orderSettings,
|
OrderSettings orderSettings,
|
||||||
ShippingSettings shippingSettings,
|
ShippingSettings shippingSettings,
|
||||||
IUrlRecordService urlRecordService,
|
IUrlRecordService urlRecordService,
|
||||||
TaxSettings taxSettings
|
TaxSettings taxSettings,
|
||||||
) : base(addressSettings,
|
IGenericAttributeService genericAttributeService
|
||||||
catalogSettings,
|
) : base(addressSettings,
|
||||||
currencySettings,
|
catalogSettings,
|
||||||
actionContextAccessor,
|
currencySettings,
|
||||||
addressModelFactory,
|
actionContextAccessor,
|
||||||
addressService,
|
addressModelFactory,
|
||||||
affiliateService,
|
addressService,
|
||||||
baseAdminModelFactory,
|
affiliateService,
|
||||||
countryService,
|
baseAdminModelFactory,
|
||||||
currencyService,
|
countryService,
|
||||||
customerService,
|
currencyService,
|
||||||
dateTimeHelper,
|
customerService,
|
||||||
discountService,
|
dateTimeHelper,
|
||||||
downloadService,
|
discountService,
|
||||||
encryptionService,
|
downloadService,
|
||||||
giftCardService,
|
encryptionService,
|
||||||
localizationService,
|
giftCardService,
|
||||||
measureService,
|
localizationService,
|
||||||
orderProcessingService,
|
measureService,
|
||||||
orderReportService,
|
orderProcessingService,
|
||||||
orderService,
|
orderReportService,
|
||||||
paymentPluginManager,
|
orderService,
|
||||||
paymentService,
|
paymentPluginManager,
|
||||||
pictureService,
|
paymentService,
|
||||||
priceCalculationService,
|
pictureService,
|
||||||
priceFormatter,
|
priceCalculationService,
|
||||||
productAttributeService,
|
priceFormatter,
|
||||||
productService,
|
productAttributeService,
|
||||||
returnRequestService,
|
productService,
|
||||||
|
returnRequestService,
|
||||||
rewardPointService,
|
rewardPointService,
|
||||||
settingService,
|
settingService,
|
||||||
shipmentService,
|
shipmentService,
|
||||||
|
|
@ -137,6 +140,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_orderMeasurementService = orderMeasurementService;
|
_orderMeasurementService = orderMeasurementService;
|
||||||
|
_genericAttributeService = genericAttributeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<OrderSearchModel> PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
|
public override async Task<OrderSearchModel> PrepareOrderSearchModelAsync(OrderSearchModel searchModel)
|
||||||
|
|
@ -169,7 +173,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<OrderListModel> PrepareOrderListModelAsync(OrderSearchModel searchModel)
|
public override async Task<OrderListModel> PrepareOrderListModelAsync(OrderSearchModel searchModel)
|
||||||
=>await base.PrepareOrderListModelAsync(searchModel);
|
=> await base.PrepareOrderListModelAsync(searchModel);
|
||||||
|
|
||||||
public async Task<OrderListModelExtended> PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel)
|
public async Task<OrderListModelExtended> PrepareOrderListModelExtendedAsync(OrderSearchModel searchModel)
|
||||||
{
|
{
|
||||||
|
|
@ -182,6 +186,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended);
|
PropertyHelper.CopyPublicValueTypeProperties(orderModel, orderModelExtended);
|
||||||
|
|
||||||
orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel);
|
orderModelExtended.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(orderModel);
|
||||||
|
orderModelExtended.DateOfReceipt = await GetPickupDateTimeAsync(orderModel);
|
||||||
|
|
||||||
Console.WriteLine(orderModelExtended.Id);
|
Console.WriteLine(orderModelExtended.Id);
|
||||||
extendedRows.Add(orderModelExtended);
|
extendedRows.Add(orderModelExtended);
|
||||||
|
|
@ -210,7 +215,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
|
||||||
return await Task.FromResult(false);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
|
|
||||||
public Task<IList<string>> GetWidgetZonesAsync()
|
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)
|
//public string GetWidgetViewComponentName(string widgetZone)
|
||||||
|
|
@ -138,6 +138,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
|
||||||
{
|
{
|
||||||
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
|
return zones.Any(widgetZone.Equals) ? typeof(ProductAttributesViewComponent) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (widgetZone == AdminWidgetZones.OrderDetailsBlock)
|
||||||
|
{
|
||||||
|
return zones.Any(widgetZone.Equals) ? typeof(OrderAttributesViewComponent) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,11 @@ public class RouteProvider : IRouteProvider
|
||||||
name: "Plugin.FruitBank.Admin.Products.ProductList",
|
name: "Plugin.FruitBank.Admin.Products.ProductList",
|
||||||
pattern: "Admin/Product/ProductList",
|
pattern: "Admin/Product/ProductList",
|
||||||
defaults: new { controller = "CustomProduct", action = "ProductList", area = AreaNames.ADMIN });
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
|
||||||
public partial record OrderModelExtended : OrderModel
|
public partial record OrderModelExtended : OrderModel
|
||||||
{
|
{
|
||||||
public bool IsMeasurable { get; set; }
|
public bool IsMeasurable { get; set; }
|
||||||
|
public DateTime? DateOfReceipt { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
|
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Services\InnvoiceApiService.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="logo.jpg" />
|
<None Remove="logo.jpg" />
|
||||||
|
|
@ -28,7 +31,10 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||||
<PackageReference Include="PdfPig" Version="0.1.11" />
|
<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="SendGrid" Version="9.29.3" />
|
||||||
|
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||||
|
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -151,9 +157,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Areas\Admin\Components\_DocumentsGridPartial.cshtml">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
@ -616,6 +619,9 @@
|
||||||
<None Update="Views\ProductAIListWidget.cshtml">
|
<None Update="Views\ProductAIListWidget.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="Views\OrderAttributes.cshtml">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="Views\ProductAttributes.cshtml">
|
<None Update="Views\ProductAttributes.cshtml">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|
@ -631,6 +637,10 @@
|
||||||
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Services\InnvoiceApiService.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- This target execute after "Build" target -->
|
<!-- This target execute after "Build" target -->
|
||||||
<Target Name="NopTarget" AfterTargets="Build">
|
<Target Name="NopTarget" AfterTargets="Build">
|
||||||
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,6 @@
|
||||||
using Azure;
|
using Nop.Core;
|
||||||
using Nop.Core;
|
|
||||||
using Nop.Core.Domain.Customers;
|
using Nop.Core.Domain.Customers;
|
||||||
using Nop.Core.Domain.Stores;
|
|
||||||
using Nop.Plugin.Misc.FruitBankPlugin.Helpers;
|
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
|
namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
{
|
{
|
||||||
|
|
@ -37,16 +29,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
return response;
|
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)
|
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}";
|
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;
|
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;
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
|
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 OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
|
||||||
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
|
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)
|
public OpenAIApiService(ISettingService settingService, HttpClient httpClient)
|
||||||
{
|
{
|
||||||
|
|
@ -286,66 +290,371 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region === PDF ANALYSIS (NEW) ===
|
#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);
|
private async Task EnsureAssistantAndVectorStoreAsync()
|
||||||
if (!uploadResponse.IsSuccessStatusCode)
|
{
|
||||||
|
// Find or create vector store
|
||||||
|
if (_vectorStoreId == null)
|
||||||
{
|
{
|
||||||
var error = await uploadResponse.Content.ReadAsStringAsync();
|
_vectorStoreId = await FindOrCreateVectorStoreAsync("pdf-analysis-store");
|
||||||
throw new Exception($"File upload failed: {error}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using var uploadJson = await JsonDocument.ParseAsync(await uploadResponse.Content.ReadAsStreamAsync());
|
// Find or create assistant
|
||||||
var fileId = uploadJson.RootElement.GetProperty("id").GetString();
|
if (_assistantId == null)
|
||||||
|
|
||||||
// Step 2: Ask model with file reference
|
|
||||||
var requestBody = new
|
|
||||||
{
|
{
|
||||||
model = "gpt-4.1", // must support file_search
|
_assistantId = await FindOrCreateAssistantAsync("PDF and Image Analyzer Assistant");
|
||||||
messages = new[]
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//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." },
|
var id = assistant.GetProperty("id").GetString();
|
||||||
new { role = "user", content = userPrompt }
|
var name = assistant.GetProperty("name").GetString();
|
||||||
},
|
|
||||||
tools = new[]
|
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
|
tool_resources = new
|
||||||
{
|
{
|
||||||
file_search = new
|
file_search = new
|
||||||
{
|
{
|
||||||
vector_store_ids = new string[] { fileId! }
|
vector_store_ids = new[] { _vectorStoreId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
|
var request = CreateAssistantRequest(
|
||||||
{
|
HttpMethod.Post,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
$"{BaseUrl}/threads/{threadId}/runs",
|
||||||
});
|
body
|
||||||
|
);
|
||||||
|
|
||||||
var requestContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
|
var response = await _httpClient.SendAsync(request);
|
||||||
var chatResponse = await _httpClient.PostAsync(OpenAiEndpoint, requestContent);
|
await EnsureSuccessAsync(response, "create run");
|
||||||
chatResponse.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
using var responseJson = await JsonDocument.ParseAsync(await chatResponse.Content.ReadAsStreamAsync());
|
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
|
||||||
var result = responseJson.RootElement
|
return json.RootElement.GetProperty("id").GetString()!;
|
||||||
.GetProperty("choices")[0]
|
|
||||||
.GetProperty("message")
|
|
||||||
.GetProperty("content")
|
|
||||||
.GetString();
|
|
||||||
|
|
||||||
return result ?? "No response from model";
|
|
||||||
}
|
}
|
||||||
#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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@* File: Plugins/Nop.Plugin.YourCompany.ProductAttributes/Views/ProductCustomAttributes.cshtml *@
|
|
||||||
|
|
||||||
@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel
|
@model Nop.Plugin.Misc.FruitBankPlugin.Models.ProductAttributesModel
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue