-
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
|
||||
public class ArticoliController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public ArticoliController(AppollinareDbContext context)
|
||||
public ArticoliController(AppollinareDbContext context, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -60,6 +63,13 @@ public class ArticoliController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Articolo>> CreateArticolo(Articolo articolo)
|
||||
{
|
||||
// Genera codice automatico
|
||||
var codice = await _autoCodeService.GenerateNextCodeAsync("articolo");
|
||||
if (!string.IsNullOrEmpty(codice))
|
||||
{
|
||||
articolo.Codice = codice;
|
||||
}
|
||||
|
||||
articolo.CreatedAt = DateTime.UtcNow;
|
||||
_context.Articoli.Add(articolo);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
240
src/Apollinare.API/Controllers/AutoCodesController.cs
Normal file
240
src/Apollinare.API/Controllers/AutoCodesController.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Apollinare.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AutoCodesController : ControllerBase
|
||||
{
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
private readonly ILogger<AutoCodesController> _logger;
|
||||
|
||||
public AutoCodesController(AutoCodeService autoCodeService, ILogger<AutoCodesController> logger)
|
||||
{
|
||||
_autoCodeService = autoCodeService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutte le configurazioni AutoCode.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<AutoCodeDto>>> GetAll()
|
||||
{
|
||||
var configs = await _autoCodeService.GetAllConfigurationsAsync();
|
||||
return Ok(configs.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le configurazioni AutoCode per un modulo specifico.
|
||||
/// </summary>
|
||||
[HttpGet("module/{moduleCode}")]
|
||||
public async Task<ActionResult<List<AutoCodeDto>>> GetByModule(string moduleCode)
|
||||
{
|
||||
var configs = await _autoCodeService.GetConfigurationsByModuleAsync(moduleCode);
|
||||
return Ok(configs.Select(ToDto).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene una configurazione AutoCode specifica.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}")]
|
||||
public async Task<ActionResult<AutoCodeDto>> Get(string entityCode)
|
||||
{
|
||||
var config = await _autoCodeService.GetConfigurationAsync(entityCode);
|
||||
if (config == null)
|
||||
return NotFound($"Configurazione per '{entityCode}' non trovata");
|
||||
|
||||
return Ok(ToDto(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera un nuovo codice per un'entità specifica.
|
||||
/// </summary>
|
||||
[HttpPost("{entityCode}/generate")]
|
||||
public async Task<ActionResult<GenerateCodeResponse>> GenerateCode(string entityCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var code = await _autoCodeService.GenerateNextCodeAsync(entityCode);
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
|
||||
}
|
||||
|
||||
return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
|
||||
return StatusCode(500, new { error = "Errore durante la generazione del codice" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene un'anteprima del prossimo codice senza incrementare la sequenza.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}/preview")]
|
||||
public async Task<ActionResult<GenerateCodeResponse>> PreviewCode(string entityCode)
|
||||
{
|
||||
var code = await _autoCodeService.PreviewNextCodeAsync(entityCode);
|
||||
if (code == null)
|
||||
{
|
||||
return BadRequest(new { error = "Generazione automatica disabilitata o configurazione non trovata" });
|
||||
}
|
||||
|
||||
return Ok(new GenerateCodeResponse { Code = code, EntityCode = entityCode, IsPreview = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna una configurazione AutoCode.
|
||||
/// </summary>
|
||||
[HttpPut("{id:int}")]
|
||||
public async Task<ActionResult<AutoCodeDto>> Update(int id, [FromBody] AutoCodeUpdateDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = await _autoCodeService.UpdateConfigurationAsync(id, dto);
|
||||
return Ok(ToDto(config));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta la sequenza per un'entità specifica.
|
||||
/// </summary>
|
||||
[HttpPost("{entityCode}/reset-sequence")]
|
||||
public async Task<ActionResult> ResetSequence(string entityCode, [FromBody] ResetSequenceRequest? request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _autoCodeService.ResetSequenceAsync(entityCode, request?.NewValue ?? 0);
|
||||
return Ok(new { message = $"Sequenza resettata per '{entityCode}'" });
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se un codice è unico per un'entità.
|
||||
/// </summary>
|
||||
[HttpGet("{entityCode}/check-unique")]
|
||||
public async Task<ActionResult<CheckUniqueResponse>> CheckUnique(
|
||||
string entityCode,
|
||||
[FromQuery] string code,
|
||||
[FromQuery] int? excludeId = null)
|
||||
{
|
||||
var isUnique = await _autoCodeService.IsCodeUniqueAsync(entityCode, code, excludeId);
|
||||
return Ok(new CheckUniqueResponse { IsUnique = isUnique, Code = code, EntityCode = entityCode });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene i placeholder disponibili per i pattern.
|
||||
/// </summary>
|
||||
[HttpGet("placeholders")]
|
||||
public ActionResult<List<PlaceholderInfo>> GetPlaceholders()
|
||||
{
|
||||
var placeholders = new List<PlaceholderInfo>
|
||||
{
|
||||
new() { Placeholder = "{PREFIX}", Description = "Prefisso configurato", Example = "ART" },
|
||||
new() { Placeholder = "{SEQ:n}", Description = "Sequenza numerica con n cifre", Example = "{SEQ:5} → 00001" },
|
||||
new() { Placeholder = "{YYYY}", Description = "Anno a 4 cifre", Example = "2025" },
|
||||
new() { Placeholder = "{YY}", Description = "Anno a 2 cifre", Example = "25" },
|
||||
new() { Placeholder = "{MM}", Description = "Mese a 2 cifre", Example = "11" },
|
||||
new() { Placeholder = "{DD}", Description = "Giorno a 2 cifre", Example = "29" },
|
||||
new() { Placeholder = "{YEAR}", Description = "Alias per {YYYY}", Example = "2025" },
|
||||
new() { Placeholder = "{MONTH}", Description = "Alias per {MM}", Example = "11" },
|
||||
new() { Placeholder = "{DAY}", Description = "Alias per {DD}", Example = "29" },
|
||||
};
|
||||
|
||||
return Ok(placeholders);
|
||||
}
|
||||
|
||||
private static AutoCodeDto ToDto(AutoCode entity)
|
||||
{
|
||||
return new AutoCodeDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
EntityCode = entity.EntityCode,
|
||||
EntityName = entity.EntityName,
|
||||
Prefix = entity.Prefix,
|
||||
Pattern = entity.Pattern,
|
||||
LastSequence = entity.LastSequence,
|
||||
ResetSequenceYearly = entity.ResetSequenceYearly,
|
||||
ResetSequenceMonthly = entity.ResetSequenceMonthly,
|
||||
LastResetYear = entity.LastResetYear,
|
||||
LastResetMonth = entity.LastResetMonth,
|
||||
IsEnabled = entity.IsEnabled,
|
||||
IsReadOnly = entity.IsReadOnly,
|
||||
ModuleCode = entity.ModuleCode,
|
||||
Description = entity.Description,
|
||||
SortOrder = entity.SortOrder,
|
||||
ExampleCode = entity.GetExampleCode(),
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region DTOs
|
||||
|
||||
public class AutoCodeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
public string EntityName { get; set; } = string.Empty;
|
||||
public string? Prefix { get; set; }
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
public long LastSequence { get; set; }
|
||||
public bool ResetSequenceYearly { get; set; }
|
||||
public bool ResetSequenceMonthly { get; set; }
|
||||
public int? LastResetYear { get; set; }
|
||||
public int? LastResetMonth { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsReadOnly { get; set; }
|
||||
public string? ModuleCode { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public string ExampleCode { get; set; } = string.Empty;
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class GenerateCodeResponse
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
public bool IsPreview { get; set; }
|
||||
}
|
||||
|
||||
public class ResetSequenceRequest
|
||||
{
|
||||
public long NewValue { get; set; } = 0;
|
||||
}
|
||||
|
||||
public class CheckUniqueResponse
|
||||
{
|
||||
public bool IsUnique { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PlaceholderInfo
|
||||
{
|
||||
public string Placeholder { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Example { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -10,10 +11,12 @@ namespace Apollinare.API.Controllers;
|
||||
public class ClientiController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public ClientiController(AppollinareDbContext context)
|
||||
public ClientiController(AppollinareDbContext context, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -47,6 +50,13 @@ public class ClientiController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<Cliente>> CreateCliente(Cliente cliente)
|
||||
{
|
||||
// Genera codice automatico
|
||||
var codice = await _autoCodeService.GenerateNextCodeAsync("cliente");
|
||||
if (!string.IsNullOrEmpty(codice))
|
||||
{
|
||||
cliente.Codice = codice;
|
||||
}
|
||||
|
||||
cliente.CreatedAt = DateTime.UtcNow;
|
||||
_context.Clienti.Add(cliente);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Apollinare.API.Hubs;
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Domain.Enums;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
@@ -13,11 +14,13 @@ public class EventiController : ControllerBase
|
||||
{
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly DataNotificationService _notifier;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
public EventiController(AppollinareDbContext context, DataNotificationService notifier)
|
||||
public EventiController(AppollinareDbContext context, DataNotificationService notifier, AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_notifier = notifier;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -343,6 +346,12 @@ public class EventiController : ControllerBase
|
||||
|
||||
private async Task<string> GeneraCodiceEvento()
|
||||
{
|
||||
// Usa AutoCodeService per generare il codice
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("evento");
|
||||
if (generatedCode != null)
|
||||
return generatedCode;
|
||||
|
||||
// Fallback: metodo legacy
|
||||
var anno = DateTime.Now.Year;
|
||||
var ultimoEvento = await _context.Eventi
|
||||
.Where(e => e.Codice != null && e.Codice.StartsWith($"EV{anno}"))
|
||||
|
||||
@@ -239,6 +239,7 @@ public class WarehouseArticlesController : ControllerBase
|
||||
public record ArticleDto(
|
||||
int Id,
|
||||
string Code,
|
||||
string? AlternativeCode,
|
||||
string Description,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
@@ -273,36 +274,36 @@ public class WarehouseArticlesController : ControllerBase
|
||||
);
|
||||
|
||||
public record CreateArticleDto(
|
||||
string Code,
|
||||
string Description,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
string? ManufacturerCode,
|
||||
int? CategoryId,
|
||||
string UnitOfMeasure,
|
||||
string? SecondaryUnitOfMeasure,
|
||||
decimal? UnitConversionFactor,
|
||||
StockManagementType StockManagement,
|
||||
bool IsBatchManaged,
|
||||
bool IsSerialManaged,
|
||||
bool HasExpiry,
|
||||
int? ExpiryWarningDays,
|
||||
decimal? MinimumStock,
|
||||
decimal? MaximumStock,
|
||||
decimal? ReorderPoint,
|
||||
decimal? ReorderQuantity,
|
||||
int? LeadTimeDays,
|
||||
ValuationMethod? ValuationMethod,
|
||||
decimal? StandardCost,
|
||||
decimal? BaseSellingPrice,
|
||||
decimal? Weight,
|
||||
decimal? Volume,
|
||||
string? Notes
|
||||
string? AlternativeCode = null,
|
||||
string? ShortDescription = null,
|
||||
string? Barcode = null,
|
||||
string? ManufacturerCode = null,
|
||||
int? CategoryId = null,
|
||||
string? SecondaryUnitOfMeasure = null,
|
||||
decimal? UnitConversionFactor = null,
|
||||
StockManagementType StockManagement = StockManagementType.Standard,
|
||||
bool IsBatchManaged = false,
|
||||
bool IsSerialManaged = false,
|
||||
bool HasExpiry = false,
|
||||
int? ExpiryWarningDays = null,
|
||||
decimal? MinimumStock = null,
|
||||
decimal? MaximumStock = null,
|
||||
decimal? ReorderPoint = null,
|
||||
decimal? ReorderQuantity = null,
|
||||
int? LeadTimeDays = null,
|
||||
ValuationMethod? ValuationMethod = null,
|
||||
decimal? StandardCost = null,
|
||||
decimal? BaseSellingPrice = null,
|
||||
decimal? Weight = null,
|
||||
decimal? Volume = null,
|
||||
string? Notes = null
|
||||
);
|
||||
|
||||
public record UpdateArticleDto(
|
||||
string Code,
|
||||
string Description,
|
||||
string? AlternativeCode,
|
||||
string? ShortDescription,
|
||||
string? Barcode,
|
||||
string? ManufacturerCode,
|
||||
@@ -363,6 +364,7 @@ public class WarehouseArticlesController : ControllerBase
|
||||
private static ArticleDto MapToDto(WarehouseArticle article) => new(
|
||||
article.Id,
|
||||
article.Code,
|
||||
article.AlternativeCode,
|
||||
article.Description,
|
||||
article.ShortDescription,
|
||||
article.Barcode,
|
||||
@@ -398,7 +400,8 @@ public class WarehouseArticlesController : ControllerBase
|
||||
|
||||
private static WarehouseArticle MapFromDto(CreateArticleDto dto) => new()
|
||||
{
|
||||
Code = dto.Code,
|
||||
// Code viene generato automaticamente da WarehouseService.CreateArticleAsync
|
||||
AlternativeCode = dto.AlternativeCode,
|
||||
Description = dto.Description,
|
||||
ShortDescription = dto.ShortDescription,
|
||||
Barcode = dto.Barcode,
|
||||
@@ -428,7 +431,8 @@ public class WarehouseArticlesController : ControllerBase
|
||||
|
||||
private static void UpdateFromDto(WarehouseArticle article, UpdateArticleDto dto)
|
||||
{
|
||||
article.Code = dto.Code;
|
||||
// Code non viene aggiornato - è generato automaticamente e immutabile
|
||||
article.AlternativeCode = dto.AlternativeCode;
|
||||
article.Description = dto.Description;
|
||||
article.ShortDescription = dto.ShortDescription;
|
||||
article.Barcode = dto.Barcode;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Apollinare.API.Services;
|
||||
using Apollinare.Domain.Entities.Warehouse;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -13,6 +14,7 @@ public class WarehouseService : IWarehouseService
|
||||
private readonly AppollinareDbContext _context;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<WarehouseService> _logger;
|
||||
private readonly AutoCodeService _autoCodeService;
|
||||
|
||||
private const string WAREHOUSES_CACHE_KEY = "warehouse_locations";
|
||||
private const string CATEGORIES_CACHE_KEY = "warehouse_categories";
|
||||
@@ -22,11 +24,13 @@ public class WarehouseService : IWarehouseService
|
||||
public WarehouseService(
|
||||
AppollinareDbContext context,
|
||||
IMemoryCache cache,
|
||||
ILogger<WarehouseService> logger)
|
||||
ILogger<WarehouseService> logger,
|
||||
AutoCodeService autoCodeService)
|
||||
{
|
||||
_context = context;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_autoCodeService = autoCodeService;
|
||||
}
|
||||
|
||||
#region Articoli
|
||||
@@ -118,6 +122,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseArticle> CreateArticleAsync(WarehouseArticle article)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(article.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_article");
|
||||
if (generatedCode != null)
|
||||
article.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per l'articolo");
|
||||
}
|
||||
|
||||
// Verifica unicità codice
|
||||
if (await _context.WarehouseArticles.AnyAsync(a => a.Code == article.Code))
|
||||
throw new InvalidOperationException($"Esiste già un articolo con codice '{article.Code}'");
|
||||
@@ -230,6 +244,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseArticleCategory> CreateCategoryAsync(WarehouseArticleCategory category)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(category.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_category");
|
||||
if (generatedCode != null)
|
||||
category.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per la categoria");
|
||||
}
|
||||
|
||||
if (await _context.WarehouseArticleCategories.AnyAsync(c => c.Code == category.Code))
|
||||
throw new InvalidOperationException($"Esiste già una categoria con codice '{category.Code}'");
|
||||
|
||||
@@ -336,6 +360,16 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<WarehouseLocation> CreateWarehouseAsync(WarehouseLocation warehouse)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(warehouse.Code))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("warehouse_location");
|
||||
if (generatedCode != null)
|
||||
warehouse.Code = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare codice automatico per il magazzino");
|
||||
}
|
||||
|
||||
if (await _context.WarehouseLocations.AnyAsync(w => w.Code == warehouse.Code))
|
||||
throw new InvalidOperationException($"Esiste già un magazzino con codice '{warehouse.Code}'");
|
||||
|
||||
@@ -464,6 +498,16 @@ public class WarehouseService : IWarehouseService
|
||||
if (!article.IsBatchManaged)
|
||||
throw new InvalidOperationException("L'articolo non è gestito a lotti");
|
||||
|
||||
// Genera numero lotto automaticamente se non specificato
|
||||
if (string.IsNullOrWhiteSpace(batch.BatchNumber))
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("article_batch");
|
||||
if (generatedCode != null)
|
||||
batch.BatchNumber = generatedCode;
|
||||
else
|
||||
throw new InvalidOperationException("Impossibile generare numero lotto automatico");
|
||||
}
|
||||
|
||||
// Verifica unicità batch number per articolo
|
||||
if (await _context.ArticleBatches.AnyAsync(b => b.ArticleId == batch.ArticleId && b.BatchNumber == batch.BatchNumber))
|
||||
throw new InvalidOperationException($"Esiste già un lotto '{batch.BatchNumber}' per questo articolo");
|
||||
@@ -809,9 +853,15 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<StockMovement> CreateMovementAsync(StockMovement movement)
|
||||
{
|
||||
// Genera numero documento se non specificato
|
||||
// Genera numero documento automaticamente se non specificato
|
||||
if (string.IsNullOrEmpty(movement.DocumentNumber))
|
||||
movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type);
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("stock_movement");
|
||||
if (generatedCode != null)
|
||||
movement.DocumentNumber = generatedCode;
|
||||
else
|
||||
movement.DocumentNumber = await GenerateDocumentNumberAsync(movement.Type); // Fallback
|
||||
}
|
||||
|
||||
// Verifica unicità documento
|
||||
if (await _context.StockMovements.AnyAsync(m => m.DocumentNumber == movement.DocumentNumber))
|
||||
@@ -1428,8 +1478,15 @@ public class WarehouseService : IWarehouseService
|
||||
|
||||
public async Task<InventoryCount> CreateInventoryCountAsync(InventoryCount inventory)
|
||||
{
|
||||
// Genera codice automaticamente se non specificato
|
||||
if (string.IsNullOrEmpty(inventory.Code))
|
||||
inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}";
|
||||
{
|
||||
var generatedCode = await _autoCodeService.GenerateNextCodeAsync("inventory_count");
|
||||
if (generatedCode != null)
|
||||
inventory.Code = generatedCode;
|
||||
else
|
||||
inventory.Code = $"INV/{DateTime.UtcNow:yyyyMMdd}/{await GenerateInventorySequenceAsync()}"; // Fallback
|
||||
}
|
||||
|
||||
inventory.CreatedAt = DateTime.UtcNow;
|
||||
_context.InventoryCounts.Add(inventory);
|
||||
|
||||
@@ -19,6 +19,7 @@ builder.Services.AddScoped<EventoCostiService>();
|
||||
builder.Services.AddScoped<DemoDataService>();
|
||||
builder.Services.AddScoped<ReportGeneratorService>();
|
||||
builder.Services.AddScoped<ModuleService>();
|
||||
builder.Services.AddScoped<AutoCodeService>();
|
||||
builder.Services.AddSingleton<DataNotificationService>();
|
||||
|
||||
// Warehouse Module Services
|
||||
@@ -100,6 +101,10 @@ using (var scope = app.Services.CreateScope())
|
||||
// Seed warehouse default data
|
||||
var warehouseService = scope.ServiceProvider.GetRequiredService<IWarehouseService>();
|
||||
await warehouseService.SeedDefaultDataAsync();
|
||||
|
||||
// Seed AutoCode configurations
|
||||
var autoCodeService = scope.ServiceProvider.GetRequiredService<AutoCodeService>();
|
||||
await autoCodeService.SeedDefaultConfigurationsAsync();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
||||
489
src/Apollinare.API/Services/AutoCodeService.cs
Normal file
489
src/Apollinare.API/Services/AutoCodeService.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Apollinare.Domain.Entities;
|
||||
using Apollinare.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Apollinare.API.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Servizio per la generazione automatica di codici univoci.
|
||||
/// Thread-safe grazie all'uso di transazioni database.
|
||||
/// </summary>
|
||||
public class AutoCodeService
|
||||
{
|
||||
private readonly AppollinareDbContext _db;
|
||||
private readonly ILogger<AutoCodeService> _logger;
|
||||
private static readonly Regex SequencePattern = new(@"\{SEQ:(\d+)\}", RegexOptions.Compiled);
|
||||
|
||||
public AutoCodeService(AppollinareDbContext db, ILogger<AutoCodeService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera il prossimo codice per un'entità specifica.
|
||||
/// Incrementa automaticamente la sequenza e gestisce i reset periodici.
|
||||
/// </summary>
|
||||
/// <param name="entityCode">Codice dell'entità (es. "warehouse_article")</param>
|
||||
/// <returns>Il nuovo codice generato, o null se la generazione automatica è disabilitata</returns>
|
||||
public async Task<string?> GenerateNextCodeAsync(string entityCode)
|
||||
{
|
||||
// Usa una transazione per garantire atomicità
|
||||
await using var transaction = await _db.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
_logger.LogWarning("Configurazione AutoCode non trovata per entità: {EntityCode}", entityCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!config.IsEnabled)
|
||||
{
|
||||
_logger.LogDebug("Generazione automatica disabilitata per entità: {EntityCode}", entityCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Gestione reset sequenza
|
||||
if (ShouldResetSequence(config, now))
|
||||
{
|
||||
config.LastSequence = 0;
|
||||
config.LastResetYear = now.Year;
|
||||
config.LastResetMonth = now.Month;
|
||||
_logger.LogInformation("Sequenza resettata per entità {EntityCode} (Anno: {Year}, Mese: {Month})",
|
||||
entityCode, now.Year, now.Month);
|
||||
}
|
||||
|
||||
// Incrementa sequenza
|
||||
config.LastSequence++;
|
||||
config.UpdatedAt = now;
|
||||
|
||||
// Genera codice dal pattern
|
||||
var code = GenerateCodeFromPattern(config.Pattern, config.Prefix, config.LastSequence, now);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogDebug("Codice generato per {EntityCode}: {Code}", entityCode, code);
|
||||
return code;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Errore durante la generazione del codice per {EntityCode}", entityCode);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Genera un codice di anteprima senza incrementare la sequenza.
|
||||
/// Utile per mostrare all'utente cosa verrà generato.
|
||||
/// </summary>
|
||||
public async Task<string?> PreviewNextCodeAsync(string entityCode)
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
|
||||
if (config == null || !config.IsEnabled)
|
||||
return null;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var nextSequence = config.LastSequence + 1;
|
||||
|
||||
// Simula reset se necessario
|
||||
if (ShouldResetSequence(config, now))
|
||||
nextSequence = 1;
|
||||
|
||||
return GenerateCodeFromPattern(config.Pattern, config.Prefix, nextSequence, now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se un codice è già utilizzato per un'entità.
|
||||
/// </summary>
|
||||
public async Task<bool> IsCodeUniqueAsync(string entityCode, string code, int? excludeId = null)
|
||||
{
|
||||
return entityCode switch
|
||||
{
|
||||
"warehouse_article" => !await _db.WarehouseArticles
|
||||
.AnyAsync(a => a.Code == code && (excludeId == null || a.Id != excludeId)),
|
||||
|
||||
"warehouse_location" => !await _db.WarehouseLocations
|
||||
.AnyAsync(w => w.Code == code && (excludeId == null || w.Id != excludeId)),
|
||||
|
||||
"warehouse_category" => !await _db.WarehouseArticleCategories
|
||||
.AnyAsync(c => c.Code == code && (excludeId == null || c.Id != excludeId)),
|
||||
|
||||
"stock_movement" => !await _db.StockMovements
|
||||
.AnyAsync(m => m.DocumentNumber == code && (excludeId == null || m.Id != excludeId)),
|
||||
|
||||
"inventory_count" => !await _db.InventoryCounts
|
||||
.AnyAsync(i => i.Code == code && (excludeId == null || i.Id != excludeId)),
|
||||
|
||||
"cliente" => !await _db.Clienti
|
||||
.AnyAsync(c => c.Codice == code && (excludeId == null || c.Id != excludeId)),
|
||||
|
||||
"evento" => !await _db.Eventi
|
||||
.AnyAsync(e => e.Codice == code && (excludeId == null || e.Id != excludeId)),
|
||||
|
||||
"articolo" => !await _db.Articoli
|
||||
.AnyAsync(a => a.Codice == code && (excludeId == null || a.Id != excludeId)),
|
||||
|
||||
_ => true // Entità non gestita, assume codice unico
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutte le configurazioni AutoCode.
|
||||
/// </summary>
|
||||
public async Task<List<AutoCode>> GetAllConfigurationsAsync()
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.OrderBy(c => c.ModuleCode)
|
||||
.ThenBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.EntityName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le configurazioni AutoCode per un modulo specifico.
|
||||
/// </summary>
|
||||
public async Task<List<AutoCode>> GetConfigurationsByModuleAsync(string moduleCode)
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.Where(c => c.ModuleCode == moduleCode)
|
||||
.OrderBy(c => c.SortOrder)
|
||||
.ThenBy(c => c.EntityName)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene una configurazione AutoCode per codice entità.
|
||||
/// </summary>
|
||||
public async Task<AutoCode?> GetConfigurationAsync(string entityCode)
|
||||
{
|
||||
return await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna una configurazione AutoCode.
|
||||
/// </summary>
|
||||
public async Task<AutoCode> UpdateConfigurationAsync(int id, AutoCodeUpdateDto dto)
|
||||
{
|
||||
var config = await _db.AutoCodes.FindAsync(id)
|
||||
?? throw new KeyNotFoundException($"Configurazione AutoCode con ID {id} non trovata");
|
||||
|
||||
if (dto.Prefix != null)
|
||||
config.Prefix = dto.Prefix;
|
||||
|
||||
if (dto.Pattern != null)
|
||||
{
|
||||
// Valida il pattern
|
||||
ValidatePattern(dto.Pattern);
|
||||
config.Pattern = dto.Pattern;
|
||||
}
|
||||
|
||||
if (dto.ResetSequenceYearly.HasValue)
|
||||
config.ResetSequenceYearly = dto.ResetSequenceYearly.Value;
|
||||
|
||||
if (dto.ResetSequenceMonthly.HasValue)
|
||||
config.ResetSequenceMonthly = dto.ResetSequenceMonthly.Value;
|
||||
|
||||
if (dto.IsEnabled.HasValue)
|
||||
config.IsEnabled = dto.IsEnabled.Value;
|
||||
|
||||
if (dto.IsReadOnly.HasValue)
|
||||
config.IsReadOnly = dto.IsReadOnly.Value;
|
||||
|
||||
if (dto.Description != null)
|
||||
config.Description = dto.Description;
|
||||
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resetta manualmente la sequenza per un'entità.
|
||||
/// </summary>
|
||||
public async Task ResetSequenceAsync(string entityCode, long newValue = 0)
|
||||
{
|
||||
var config = await _db.AutoCodes
|
||||
.FirstOrDefaultAsync(c => c.EntityCode == entityCode)
|
||||
?? throw new KeyNotFoundException($"Configurazione AutoCode per '{entityCode}' non trovata");
|
||||
|
||||
config.LastSequence = newValue;
|
||||
config.LastResetYear = DateTime.Now.Year;
|
||||
config.LastResetMonth = DateTime.Now.Month;
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Sequenza resettata manualmente per {EntityCode} a {Value}", entityCode, newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inizializza le configurazioni di default per tutte le entità.
|
||||
/// Chiamato al seed dell'applicazione.
|
||||
/// </summary>
|
||||
public async Task SeedDefaultConfigurationsAsync()
|
||||
{
|
||||
var defaults = new List<AutoCode>
|
||||
{
|
||||
// Core
|
||||
new()
|
||||
{
|
||||
EntityCode = "cliente",
|
||||
EntityName = "Cliente",
|
||||
Prefix = "CLI",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "core",
|
||||
Description = "Codice cliente (es. CLI-00001)",
|
||||
SortOrder = 10
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "evento",
|
||||
EntityName = "Evento",
|
||||
Prefix = "EVT",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "core",
|
||||
Description = "Codice evento con anno (es. EVT2025-00001)",
|
||||
SortOrder = 20
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "articolo",
|
||||
EntityName = "Articolo (Legacy)",
|
||||
Prefix = "ART",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "core",
|
||||
Description = "Codice articolo legacy (es. ART-00001)",
|
||||
SortOrder = 30
|
||||
},
|
||||
|
||||
// Warehouse module
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_location",
|
||||
EntityName = "Magazzino",
|
||||
Prefix = "MAG",
|
||||
Pattern = "{PREFIX}-{SEQ:3}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice magazzino (es. MAG-001)",
|
||||
SortOrder = 10
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_article",
|
||||
EntityName = "Articolo Magazzino",
|
||||
Prefix = "WA",
|
||||
Pattern = "{PREFIX}{SEQ:6}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice articolo magazzino (es. WA000001)",
|
||||
SortOrder = 20
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "warehouse_category",
|
||||
EntityName = "Categoria Articolo",
|
||||
Prefix = "CAT",
|
||||
Pattern = "{PREFIX}-{SEQ:4}",
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice categoria (es. CAT-0001)",
|
||||
SortOrder = 30
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "stock_movement",
|
||||
EntityName = "Movimento Magazzino",
|
||||
Prefix = "MOV",
|
||||
Pattern = "{PREFIX}{YYYY}{MM}-{SEQ:5}",
|
||||
ResetSequenceMonthly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Numero documento movimento (es. MOV202511-00001)",
|
||||
SortOrder = 40
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "inventory_count",
|
||||
EntityName = "Inventario",
|
||||
Prefix = "INV",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:4}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Codice inventario (es. INV2025-0001)",
|
||||
SortOrder = 50
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "article_batch",
|
||||
EntityName = "Lotto Articolo",
|
||||
Prefix = "LOT",
|
||||
Pattern = "{PREFIX}{YYYY}{MM}{DD}-{SEQ:4}",
|
||||
ResetSequenceMonthly = true,
|
||||
ModuleCode = "warehouse",
|
||||
Description = "Numero lotto (es. LOT20251129-0001)",
|
||||
SortOrder = 60
|
||||
},
|
||||
|
||||
// Future: Purchases module
|
||||
new()
|
||||
{
|
||||
EntityCode = "purchase_order",
|
||||
EntityName = "Ordine Acquisto",
|
||||
Prefix = "ODA",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "purchases",
|
||||
Description = "Numero ordine acquisto (es. ODA2025-00001)",
|
||||
SortOrder = 10,
|
||||
IsEnabled = false // Disabilitato finché il modulo non è implementato
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "supplier",
|
||||
EntityName = "Fornitore",
|
||||
Prefix = "FOR",
|
||||
Pattern = "{PREFIX}-{SEQ:5}",
|
||||
ModuleCode = "purchases",
|
||||
Description = "Codice fornitore (es. FOR-00001)",
|
||||
SortOrder = 20,
|
||||
IsEnabled = false
|
||||
},
|
||||
|
||||
// Future: Sales module
|
||||
new()
|
||||
{
|
||||
EntityCode = "sales_order",
|
||||
EntityName = "Ordine Vendita",
|
||||
Prefix = "ODV",
|
||||
Pattern = "{PREFIX}{YYYY}-{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "sales",
|
||||
Description = "Numero ordine vendita (es. ODV2025-00001)",
|
||||
SortOrder = 10,
|
||||
IsEnabled = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
EntityCode = "invoice",
|
||||
EntityName = "Fattura",
|
||||
Prefix = "FT",
|
||||
Pattern = "{PREFIX}{YYYY}/{SEQ:5}",
|
||||
ResetSequenceYearly = true,
|
||||
ModuleCode = "sales",
|
||||
Description = "Numero fattura (es. FT2025/00001)",
|
||||
SortOrder = 20,
|
||||
IsEnabled = false
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var config in defaults)
|
||||
{
|
||||
var exists = await _db.AutoCodes.AnyAsync(c => c.EntityCode == config.EntityCode);
|
||||
if (!exists)
|
||||
{
|
||||
config.CreatedAt = DateTime.Now;
|
||||
_db.AutoCodes.Add(config);
|
||||
_logger.LogInformation("Configurazione AutoCode creata per {EntityCode}", config.EntityCode);
|
||||
}
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private bool ShouldResetSequence(AutoCode config, DateTime now)
|
||||
{
|
||||
if (config.ResetSequenceMonthly)
|
||||
{
|
||||
return config.LastResetYear != now.Year || config.LastResetMonth != now.Month;
|
||||
}
|
||||
|
||||
if (config.ResetSequenceYearly)
|
||||
{
|
||||
return config.LastResetYear != now.Year;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GenerateCodeFromPattern(string pattern, string? prefix, long sequence, DateTime date)
|
||||
{
|
||||
var code = pattern
|
||||
.Replace("{PREFIX}", prefix ?? "")
|
||||
.Replace("{YEAR}", date.Year.ToString())
|
||||
.Replace("{YYYY}", date.Year.ToString())
|
||||
.Replace("{YY}", date.Year.ToString().Substring(2))
|
||||
.Replace("{MONTH}", date.Month.ToString("D2"))
|
||||
.Replace("{MM}", date.Month.ToString("D2"))
|
||||
.Replace("{DAY}", date.Day.ToString("D2"))
|
||||
.Replace("{DD}", date.Day.ToString("D2"));
|
||||
|
||||
// Gestisci {SEQ:n}
|
||||
code = SequencePattern.Replace(code, match =>
|
||||
{
|
||||
var digits = int.Parse(match.Groups[1].Value);
|
||||
return sequence.ToString($"D{digits}");
|
||||
});
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private static void ValidatePattern(string pattern)
|
||||
{
|
||||
// Verifica che ci sia almeno un placeholder sequenza
|
||||
if (!SequencePattern.IsMatch(pattern))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Il pattern deve contenere almeno un placeholder {SEQ:n} per la sequenza numerica");
|
||||
}
|
||||
|
||||
// Verifica che i placeholder siano validi
|
||||
var validPlaceholders = new[]
|
||||
{
|
||||
"{PREFIX}", "{YEAR}", "{YYYY}", "{YY}",
|
||||
"{MONTH}", "{MM}", "{DAY}", "{DD}"
|
||||
};
|
||||
|
||||
var placeholderRegex = new Regex(@"\{[^}]+\}");
|
||||
var matches = placeholderRegex.Matches(pattern);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var placeholder = match.Value;
|
||||
if (!SequencePattern.IsMatch(placeholder) && !validPlaceholders.Contains(placeholder))
|
||||
{
|
||||
throw new ArgumentException($"Placeholder non valido: {placeholder}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO per l'aggiornamento di una configurazione AutoCode.
|
||||
/// </summary>
|
||||
public class AutoCodeUpdateDto
|
||||
{
|
||||
public string? Prefix { get; set; }
|
||||
public string? Pattern { get; set; }
|
||||
public bool? ResetSequenceYearly { get; set; }
|
||||
public bool? ResetSequenceMonthly { get; set; }
|
||||
public bool? IsEnabled { get; set; }
|
||||
public bool? IsReadOnly { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,16 @@ namespace Apollinare.Domain.Entities;
|
||||
|
||||
public class Articolo : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice articolo - generato automaticamente
|
||||
/// </summary>
|
||||
public string Codice { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? CodiceAlternativo { get; set; }
|
||||
|
||||
public string Descrizione { get; set; } = string.Empty;
|
||||
public int? TipoMaterialeId { get; set; }
|
||||
public int? CategoriaId { get; set; }
|
||||
|
||||
117
src/Apollinare.Domain/Entities/AutoCode.cs
Normal file
117
src/Apollinare.Domain/Entities/AutoCode.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
namespace Apollinare.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Configurazione per la generazione automatica di codici.
|
||||
/// Ogni entità può avere la propria configurazione con pattern personalizzabile.
|
||||
///
|
||||
/// Pattern supportati:
|
||||
/// - {SEQ:n} - Sequenza numerica con n cifre (es. {SEQ:5} → 00001)
|
||||
/// - {YEAR} o {YYYY} - Anno corrente a 4 cifre
|
||||
/// - {YY} - Anno corrente a 2 cifre
|
||||
/// - {MONTH} o {MM} - Mese corrente a 2 cifre
|
||||
/// - {DAY} o {DD} - Giorno corrente a 2 cifre
|
||||
/// - {PREFIX} - Usa il prefisso definito
|
||||
/// - Testo statico (es. "ART-", "-", "/")
|
||||
///
|
||||
/// Esempi di pattern:
|
||||
/// - "ART-{YYYY}-{SEQ:5}" → ART-2025-00001
|
||||
/// - "{PREFIX}{YY}{MM}{SEQ:4}" → MAG2511-0001
|
||||
/// - "CLI/{YYYY}/{SEQ:6}" → CLI/2025/000001
|
||||
/// </summary>
|
||||
public class AutoCode : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco dell'entità (es. "warehouse_article", "warehouse_location", "cliente")
|
||||
/// </summary>
|
||||
public string EntityCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome visualizzato dell'entità (es. "Articolo Magazzino", "Magazzino", "Cliente")
|
||||
/// </summary>
|
||||
public string EntityName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Prefisso opzionale da usare nel pattern con {PREFIX}
|
||||
/// </summary>
|
||||
public string? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pattern per la generazione del codice
|
||||
/// </summary>
|
||||
public string Pattern { get; set; } = "{PREFIX}{SEQ:5}";
|
||||
|
||||
/// <summary>
|
||||
/// Ultimo numero di sequenza utilizzato (per {SEQ:n})
|
||||
/// </summary>
|
||||
public long LastSequence { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la sequenza viene resettata ogni anno
|
||||
/// </summary>
|
||||
public bool ResetSequenceYearly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la sequenza viene resettata ogni mese
|
||||
/// </summary>
|
||||
public bool ResetSequenceMonthly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Anno dell'ultimo reset della sequenza
|
||||
/// </summary>
|
||||
public int? LastResetYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mese dell'ultimo reset della sequenza (se ResetSequenceMonthly)
|
||||
/// </summary>
|
||||
public int? LastResetMonth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Se true, la generazione automatica è abilitata
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, il codice non può essere modificato manualmente
|
||||
/// </summary>
|
||||
public bool IsReadOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Modulo di appartenenza (es. "core", "warehouse", "purchases")
|
||||
/// </summary>
|
||||
public string? ModuleCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Descrizione della configurazione
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordine di visualizzazione nel pannello admin
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Esempio di codice generato (calcolato, non persistito)
|
||||
/// </summary>
|
||||
public string GetExampleCode()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
return Pattern
|
||||
.Replace("{PREFIX}", Prefix ?? "")
|
||||
.Replace("{YEAR}", now.Year.ToString())
|
||||
.Replace("{YYYY}", now.Year.ToString())
|
||||
.Replace("{YY}", now.Year.ToString().Substring(2))
|
||||
.Replace("{MONTH}", now.Month.ToString("D2"))
|
||||
.Replace("{MM}", now.Month.ToString("D2"))
|
||||
.Replace("{DAY}", now.Day.ToString("D2"))
|
||||
.Replace("{DD}", now.Day.ToString("D2"))
|
||||
.Replace("{SEQ:1}", "X")
|
||||
.Replace("{SEQ:2}", "XX")
|
||||
.Replace("{SEQ:3}", "XXX")
|
||||
.Replace("{SEQ:4}", "XXXX")
|
||||
.Replace("{SEQ:5}", "XXXXX")
|
||||
.Replace("{SEQ:6}", "XXXXXX")
|
||||
.Replace("{SEQ:7}", "XXXXXXX")
|
||||
.Replace("{SEQ:8}", "XXXXXXXX");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,16 @@ namespace Apollinare.Domain.Entities;
|
||||
|
||||
public class Cliente : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice cliente - generato automaticamente
|
||||
/// </summary>
|
||||
public string Codice { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? CodiceAlternativo { get; set; }
|
||||
|
||||
public string RagioneSociale { get; set; } = string.Empty;
|
||||
public string? Indirizzo { get; set; }
|
||||
public string? Cap { get; set; }
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseArticle : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco articolo (SKU)
|
||||
/// Codice univoco articolo (SKU) - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Descrizione articolo
|
||||
/// </summary>
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseArticleCategory : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice categoria
|
||||
/// Codice categoria - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nome categoria
|
||||
/// </summary>
|
||||
|
||||
@@ -6,10 +6,15 @@ namespace Apollinare.Domain.Entities.Warehouse;
|
||||
public class WarehouseLocation : BaseEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Codice univoco del magazzino (es. "MAG01", "CENTRALE")
|
||||
/// Codice univoco del magazzino - generato automaticamente
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Codice alternativo (opzionale, inserito dall'utente)
|
||||
/// </summary>
|
||||
public string? AlternativeCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nome descrittivo del magazzino
|
||||
/// </summary>
|
||||
|
||||
@@ -41,6 +41,9 @@ public class AppollinareDbContext : DbContext
|
||||
public DbSet<AppModule> AppModules => Set<AppModule>();
|
||||
public DbSet<ModuleSubscription> ModuleSubscriptions => Set<ModuleSubscription>();
|
||||
|
||||
// Auto Code system
|
||||
public DbSet<AutoCode> AutoCodes => Set<AutoCode>();
|
||||
|
||||
// Warehouse module entities
|
||||
public DbSet<WarehouseLocation> WarehouseLocations => Set<WarehouseLocation>();
|
||||
public DbSet<WarehouseArticle> WarehouseArticles => Set<WarehouseArticle>();
|
||||
@@ -274,6 +277,13 @@ public class AppollinareDbContext : DbContext
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// AutoCode
|
||||
modelBuilder.Entity<AutoCode>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.EntityCode).IsUnique();
|
||||
entity.HasIndex(e => e.ModuleCode);
|
||||
});
|
||||
|
||||
// ===============================================
|
||||
// WAREHOUSE MODULE ENTITIES
|
||||
// ===============================================
|
||||
|
||||
3091
src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
generated
Normal file
3091
src/Apollinare.Infrastructure/Migrations/20251129135918_AddAutoCodeSystem.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Apollinare.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAutoCodeSystem : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AutoCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
EntityCode = table.Column<string>(type: "TEXT", nullable: false),
|
||||
EntityName = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Prefix = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Pattern = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LastSequence = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
ResetSequenceYearly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ResetSequenceMonthly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LastResetYear = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
LastResetMonth = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
IsReadOnly = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
ModuleCode = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SortOrder = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedBy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
UpdatedBy = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AutoCodes", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoCodes_EntityCode",
|
||||
table: "AutoCodes",
|
||||
column: "EntityCode",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AutoCodes_ModuleCode",
|
||||
table: "AutoCodes",
|
||||
column: "ModuleCode");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AutoCodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
3110
src/Apollinare.Infrastructure/Migrations/20251129144249_AddAlternativeCodeFields.Designer.cs
generated
Normal file
3110
src/Apollinare.Infrastructure/Migrations/20251129144249_AddAlternativeCodeFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Apollinare.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlternativeCodeFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseLocations",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticles",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticleCategories",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Codice",
|
||||
table: "Clienti",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Clienti",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Articoli",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseLocations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlternativeCode",
|
||||
table: "WarehouseArticleCategories");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Codice",
|
||||
table: "Clienti");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Clienti");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CodiceAlternativo",
|
||||
table: "Articoli");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceAlternativo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -153,6 +156,79 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.ToTable("Articoli");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Apollinare.Domain.Entities.AutoCode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EntityCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EntityName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsReadOnly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LastResetMonth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("LastResetYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("LastSequence")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ModuleCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Pattern")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Prefix")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("ResetSequenceMonthly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ResetSequenceYearly")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EntityCode")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ModuleCode");
|
||||
|
||||
b.ToTable("AutoCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Apollinare.Domain.Entities.Cliente", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -168,6 +244,13 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.Property<string>("Citta")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Codice")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceAlternativo")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CodiceDestinatario")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -2170,6 +2253,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Barcode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -2315,6 +2401,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
@@ -2386,6 +2475,9 @@ namespace Apollinare.Infrastructure.Migrations
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AlternativeCode")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("City")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user