ADM-008: Puntos de Venta (CRUD fundacional) #19
@@ -7,11 +7,8 @@ using SIGCM2.Application.PuntosDeVenta.Create;
|
|||||||
using SIGCM2.Application.PuntosDeVenta.Deactivate;
|
using SIGCM2.Application.PuntosDeVenta.Deactivate;
|
||||||
using SIGCM2.Application.PuntosDeVenta.GetById;
|
using SIGCM2.Application.PuntosDeVenta.GetById;
|
||||||
using SIGCM2.Application.PuntosDeVenta.List;
|
using SIGCM2.Application.PuntosDeVenta.List;
|
||||||
using SIGCM2.Application.PuntosDeVenta.ProximoNumero;
|
|
||||||
using SIGCM2.Application.PuntosDeVenta.Reactivate;
|
using SIGCM2.Application.PuntosDeVenta.Reactivate;
|
||||||
using SIGCM2.Application.PuntosDeVenta.Reservar;
|
|
||||||
using SIGCM2.Application.PuntosDeVenta.Update;
|
using SIGCM2.Application.PuntosDeVenta.Update;
|
||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Api.Controllers;
|
namespace SIGCM2.Api.Controllers;
|
||||||
|
|
||||||
@@ -160,34 +157,6 @@ public sealed class PuntosDeVentaController : ControllerBase
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reserves the next sequential number for a given PdV and TipoComprobante.</summary>
|
|
||||||
[HttpPost("{id:int}/secuencias/{tipoComprobante}/reservar")]
|
|
||||||
[RequirePermission("administracion:puntos_de_venta:gestionar")]
|
|
||||||
[ProducesResponseType(typeof(ReservaNumeroDto), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
||||||
public async Task<IActionResult> ReservarNumero([FromRoute] int id, [FromRoute] TipoComprobante tipoComprobante)
|
|
||||||
{
|
|
||||||
var command = new ReservarNumeroCommand(id, tipoComprobante);
|
|
||||||
var result = await _dispatcher.Send<ReservarNumeroCommand, ReservaNumeroDto>(command);
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Returns the next available number (read-only, no reservation).</summary>
|
|
||||||
[HttpGet("{id:int}/secuencias/{tipoComprobante}/proximo")]
|
|
||||||
[RequirePermission("administracion:puntos_de_venta:gestionar")]
|
|
||||||
[ProducesResponseType(typeof(ProximoNumeroDto), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<IActionResult> GetProximoNumero([FromRoute] int id, [FromRoute] TipoComprobante tipoComprobante)
|
|
||||||
{
|
|
||||||
var query = new GetProximoNumeroQuery(id, tipoComprobante);
|
|
||||||
var result = await _dispatcher.Send<GetProximoNumeroQuery, ProximoNumeroDto>(query);
|
|
||||||
return Ok(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Request body records ──────────────────────────────────────────────────────
|
// ── Request body records ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -244,18 +244,6 @@ public sealed class ExceptionFilter : IExceptionFilter
|
|||||||
context.ExceptionHandled = true;
|
context.ExceptionHandled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PuntoDeVentaInactivoException puntoDeVentaInactivoEx:
|
|
||||||
context.Result = new ObjectResult(new
|
|
||||||
{
|
|
||||||
error = "punto_de_venta_inactivo",
|
|
||||||
message = puntoDeVentaInactivoEx.Message
|
|
||||||
})
|
|
||||||
{
|
|
||||||
StatusCode = StatusCodes.Status409Conflict
|
|
||||||
};
|
|
||||||
context.ExceptionHandled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NumeroAFIPDuplicadoException numeroAFIPDupEx:
|
case NumeroAFIPDuplicadoException numeroAFIPDupEx:
|
||||||
context.Result = new ObjectResult(new
|
context.Result = new ObjectResult(new
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using SIGCM2.Application.Common;
|
using SIGCM2.Application.Common;
|
||||||
using SIGCM2.Domain.Entities;
|
using SIGCM2.Domain.Entities;
|
||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.Abstractions.Persistence;
|
namespace SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
|
||||||
@@ -11,6 +10,4 @@ public interface IPuntoDeVentaRepository
|
|||||||
Task<bool> ExistsByNumeroAFIPInMedioAsync(int medioId, short numeroAFIP, int? excludeId = null, CancellationToken ct = default);
|
Task<bool> ExistsByNumeroAFIPInMedioAsync(int medioId, short numeroAFIP, int? excludeId = null, CancellationToken ct = default);
|
||||||
Task UpdateAsync(PuntoDeVenta pdv, CancellationToken ct = default);
|
Task UpdateAsync(PuntoDeVenta pdv, CancellationToken ct = default);
|
||||||
Task<PagedResult<PuntoDeVenta>> GetPagedAsync(PuntosDeVentaQuery q, CancellationToken ct = default);
|
Task<PagedResult<PuntoDeVenta>> GetPagedAsync(PuntosDeVentaQuery q, CancellationToken ct = default);
|
||||||
Task<int> ReservarNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default);
|
|
||||||
Task<int?> GetUltimoNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ using SIGCM2.Application.PuntosDeVenta.Create;
|
|||||||
using SIGCM2.Application.PuntosDeVenta.Deactivate;
|
using SIGCM2.Application.PuntosDeVenta.Deactivate;
|
||||||
using SIGCM2.Application.PuntosDeVenta.GetById;
|
using SIGCM2.Application.PuntosDeVenta.GetById;
|
||||||
using SIGCM2.Application.PuntosDeVenta.List;
|
using SIGCM2.Application.PuntosDeVenta.List;
|
||||||
using SIGCM2.Application.PuntosDeVenta.ProximoNumero;
|
|
||||||
using SIGCM2.Application.PuntosDeVenta.Reactivate;
|
using SIGCM2.Application.PuntosDeVenta.Reactivate;
|
||||||
using SIGCM2.Application.PuntosDeVenta.Reservar;
|
|
||||||
using SIGCM2.Application.PuntosDeVenta.Update;
|
using SIGCM2.Application.PuntosDeVenta.Update;
|
||||||
using SIGCM2.Application.Secciones.Create;
|
using SIGCM2.Application.Secciones.Create;
|
||||||
using SIGCM2.Application.Secciones.Deactivate;
|
using SIGCM2.Application.Secciones.Deactivate;
|
||||||
@@ -105,8 +103,6 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<ICommandHandler<ReactivatePuntoDeVentaCommand, PuntoDeVentaStatusDto>, ReactivatePuntoDeVentaCommandHandler>();
|
services.AddScoped<ICommandHandler<ReactivatePuntoDeVentaCommand, PuntoDeVentaStatusDto>, ReactivatePuntoDeVentaCommandHandler>();
|
||||||
services.AddScoped<ICommandHandler<ListPuntosDeVentaQuery, PagedResult<PuntoDeVentaListItemDto>>, ListPuntosDeVentaQueryHandler>();
|
services.AddScoped<ICommandHandler<ListPuntosDeVentaQuery, PagedResult<PuntoDeVentaListItemDto>>, ListPuntosDeVentaQueryHandler>();
|
||||||
services.AddScoped<ICommandHandler<GetPuntoDeVentaByIdQuery, PuntoDeVentaDetailDto>, GetPuntoDeVentaByIdQueryHandler>();
|
services.AddScoped<ICommandHandler<GetPuntoDeVentaByIdQuery, PuntoDeVentaDetailDto>, GetPuntoDeVentaByIdQueryHandler>();
|
||||||
services.AddScoped<ICommandHandler<ReservarNumeroCommand, ReservaNumeroDto>, ReservarNumeroCommandHandler>();
|
|
||||||
services.AddScoped<ICommandHandler<GetProximoNumeroQuery, ProximoNumeroDto>, GetProximoNumeroQueryHandler>();
|
|
||||||
|
|
||||||
// FluentValidation validators (scans entire Application assembly)
|
// FluentValidation validators (scans entire Application assembly)
|
||||||
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero;
|
|
||||||
|
|
||||||
public sealed record GetProximoNumeroQuery(int PuntoDeVentaId, TipoComprobante TipoComprobante);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using SIGCM2.Application.Abstractions;
|
|
||||||
using SIGCM2.Application.Abstractions.Persistence;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Consulta el próximo número disponible sin reservarlo (read-only, REQ-SEC-CMB-005).
|
|
||||||
/// Retorna UltimoNumero+1; si no existe fila devuelve 1.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GetProximoNumeroQueryHandler : ICommandHandler<GetProximoNumeroQuery, ProximoNumeroDto>
|
|
||||||
{
|
|
||||||
private readonly IPuntoDeVentaRepository _repo;
|
|
||||||
|
|
||||||
public GetProximoNumeroQueryHandler(IPuntoDeVentaRepository repo)
|
|
||||||
{
|
|
||||||
_repo = repo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ProximoNumeroDto> Handle(GetProximoNumeroQuery query)
|
|
||||||
{
|
|
||||||
var ultimoNumero = await _repo.GetUltimoNumeroAsync(query.PuntoDeVentaId, query.TipoComprobante);
|
|
||||||
|
|
||||||
var proximo = ultimoNumero.HasValue ? ultimoNumero.Value + 1 : 1;
|
|
||||||
|
|
||||||
return new ProximoNumeroDto(query.TipoComprobante, proximo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.ProximoNumero;
|
|
||||||
|
|
||||||
public sealed record ProximoNumeroDto(TipoComprobante TipoComprobante, int ProximoNumero);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.Reservar;
|
|
||||||
|
|
||||||
public sealed record ReservaNumeroDto(TipoComprobante TipoComprobante, int NumeroReservado);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.Reservar;
|
|
||||||
|
|
||||||
public sealed record ReservarNumeroCommand(int PuntoDeVentaId, TipoComprobante TipoComprobante);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using SIGCM2.Application.Abstractions;
|
|
||||||
using SIGCM2.Application.Abstractions.Persistence;
|
|
||||||
using SIGCM2.Domain.Exceptions;
|
|
||||||
|
|
||||||
namespace SIGCM2.Application.PuntosDeVenta.Reservar;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reserva el próximo número correlativo para (PdvId × TipoComprobante) ejecutando
|
|
||||||
/// usp_ReservarNumeroComprobante vía el repositorio.
|
|
||||||
///
|
|
||||||
/// NOTAS DE DISEÑO (AD4, AD9):
|
|
||||||
/// - NO se envuelve en TransactionScope: el SP ya es atómico bajo SERIALIZABLE.
|
|
||||||
/// Un TransactionScope ambiente aquí escalaría a DTC → innecesario.
|
|
||||||
/// - NO usa Polly: no está en el proyecto. Retry deadlock con bucle simple.
|
|
||||||
/// - Infrastructure traduce SqlException 1205 → DeadlockTransientException.
|
|
||||||
/// - Backoff en ms: [25, 75, 200, 500, 1200] — 5 retries máximo (6 intentos totales).
|
|
||||||
/// - La auditoría de reservas corre solo vía Temporal Tables (AD8).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ReservarNumeroCommandHandler : ICommandHandler<ReservarNumeroCommand, ReservaNumeroDto>
|
|
||||||
{
|
|
||||||
private readonly IPuntoDeVentaRepository _repo;
|
|
||||||
private readonly int[] _deadlockBackoffMs;
|
|
||||||
|
|
||||||
private static readonly int[] DefaultBackoffMs = [25, 75, 200, 500, 1200];
|
|
||||||
|
|
||||||
public ReservarNumeroCommandHandler(IPuntoDeVentaRepository repo)
|
|
||||||
: this(repo, DefaultBackoffMs) { }
|
|
||||||
|
|
||||||
/// <summary>Constructor with custom backoff for testing (e.g., [0,0,0] for fast tests).</summary>
|
|
||||||
public ReservarNumeroCommandHandler(IPuntoDeVentaRepository repo, int[] deadlockBackoffMs)
|
|
||||||
{
|
|
||||||
_repo = repo;
|
|
||||||
_deadlockBackoffMs = deadlockBackoffMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ReservaNumeroDto> Handle(ReservarNumeroCommand command)
|
|
||||||
{
|
|
||||||
for (var i = 0; ; i++)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var numero = await _repo.ReservarNumeroAsync(command.PuntoDeVentaId, command.TipoComprobante);
|
|
||||||
return new ReservaNumeroDto(command.TipoComprobante, numero);
|
|
||||||
}
|
|
||||||
catch (DeadlockTransientException) when (i < _deadlockBackoffMs.Length)
|
|
||||||
{
|
|
||||||
// Deadlock — retry with backoff
|
|
||||||
await Task.Delay(_deadlockBackoffMs[i]);
|
|
||||||
}
|
|
||||||
// All other exceptions bubble up immediately
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
namespace SIGCM2.Domain.Entities;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lleva el correlativo de números de comprobante por (PuntoDeVentaId × TipoComprobante).
|
|
||||||
/// La reserva atómica la ejecuta usp_ReservarNumeroComprobante directamente en BD.
|
|
||||||
/// Este objeto es un helper de lectura/proyección.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SecuenciaComprobante
|
|
||||||
{
|
|
||||||
public int PuntoDeVentaId { get; }
|
|
||||||
public TipoComprobante TipoComprobante { get; }
|
|
||||||
public int UltimoNumero { get; }
|
|
||||||
public DateTime FechaCreacion { get; }
|
|
||||||
public DateTime? FechaModificacion { get; }
|
|
||||||
|
|
||||||
/// <summary>El próximo número disponible (read-only, sin modificar el estado).</summary>
|
|
||||||
public int ProximoNumero => UltimoNumero + 1;
|
|
||||||
|
|
||||||
public SecuenciaComprobante(
|
|
||||||
int puntoDeVentaId,
|
|
||||||
TipoComprobante tipoComprobante,
|
|
||||||
int ultimoNumero,
|
|
||||||
DateTime fechaCreacion,
|
|
||||||
DateTime? fechaModificacion)
|
|
||||||
{
|
|
||||||
PuntoDeVentaId = puntoDeVentaId;
|
|
||||||
TipoComprobante = tipoComprobante;
|
|
||||||
UltimoNumero = ultimoNumero;
|
|
||||||
FechaCreacion = fechaCreacion;
|
|
||||||
FechaModificacion = fechaModificacion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace SIGCM2.Domain.Enums;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tipos de comprobante AFIP soportados por ADM-008.
|
|
||||||
/// Valor TINYINT persistido en BD (CHECK TipoComprobante BETWEEN 1 AND 6).
|
|
||||||
/// Migración a tabla maestra diferida a FAC-001.
|
|
||||||
/// </summary>
|
|
||||||
public enum TipoComprobante : byte
|
|
||||||
{
|
|
||||||
FacturaA = 1,
|
|
||||||
FacturaB = 2,
|
|
||||||
FacturaC = 3,
|
|
||||||
NotaCreditoA = 4,
|
|
||||||
NotaCreditoB = 5,
|
|
||||||
NotaCreditoC = 6,
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace SIGCM2.Domain.Exceptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Thrown by Infrastructure when a database deadlock (SQL 1205) is detected.
|
|
||||||
/// Allows Application handlers to retry without referencing SqlClient.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class DeadlockTransientException : DomainException
|
|
||||||
{
|
|
||||||
public DeadlockTransientException()
|
|
||||||
: base("Se detectó un deadlock en la base de datos. Reintentando operación.") { }
|
|
||||||
|
|
||||||
public DeadlockTransientException(Exception innerException)
|
|
||||||
: base("Se detectó un deadlock en la base de datos. Reintentando operación.", innerException) { }
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace SIGCM2.Domain.Exceptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Thrown when a mutation (reserva) is attempted on an inactive PuntoDeVenta.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class PuntoDeVentaInactivoException : DomainException
|
|
||||||
{
|
|
||||||
public int PuntoDeVentaId { get; }
|
|
||||||
|
|
||||||
public PuntoDeVentaInactivoException(int puntoDeVentaId)
|
|
||||||
: base($"El punto de venta {puntoDeVentaId} está inactivo. No se pueden realizar operaciones hasta reactivarlo.")
|
|
||||||
{
|
|
||||||
PuntoDeVentaId = puntoDeVentaId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
using System.Data;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using SIGCM2.Application.Abstractions.Persistence;
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
using SIGCM2.Application.Common;
|
using SIGCM2.Application.Common;
|
||||||
using SIGCM2.Domain.Entities;
|
using SIGCM2.Domain.Entities;
|
||||||
using SIGCM2.Domain.Enums;
|
|
||||||
using SIGCM2.Domain.Exceptions;
|
using SIGCM2.Domain.Exceptions;
|
||||||
|
|
||||||
namespace SIGCM2.Infrastructure.Persistence;
|
namespace SIGCM2.Infrastructure.Persistence;
|
||||||
@@ -153,56 +151,6 @@ public sealed class PuntoDeVentaRepository : IPuntoDeVentaRepository
|
|||||||
return new PagedResult<PuntoDeVenta>(items, page, pageSize, total);
|
return new PagedResult<PuntoDeVenta>(items, page, pageSize, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> ReservarNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var parameters = new DynamicParameters();
|
|
||||||
parameters.Add("PuntoDeVentaId", puntoDeVentaId, DbType.Int32);
|
|
||||||
parameters.Add("TipoComprobante", (byte)tipo, DbType.Byte);
|
|
||||||
parameters.Add("NumeroReservado", dbType: DbType.Int32, direction: ParameterDirection.Output);
|
|
||||||
|
|
||||||
await using var connection = _connectionFactory.CreateConnection();
|
|
||||||
await connection.OpenAsync(ct);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await connection.ExecuteAsync(
|
|
||||||
"dbo.usp_ReservarNumeroComprobante",
|
|
||||||
parameters,
|
|
||||||
commandType: CommandType.StoredProcedure);
|
|
||||||
}
|
|
||||||
catch (SqlException ex)
|
|
||||||
{
|
|
||||||
throw ex.Number switch
|
|
||||||
{
|
|
||||||
50001 => new PuntoDeVentaInactivoException(puntoDeVentaId),
|
|
||||||
50002 => new MedioInactivoException(puntoDeVentaId),
|
|
||||||
50003 => new PuntoDeVentaNotFoundException(puntoDeVentaId),
|
|
||||||
1205 => new DeadlockTransientException(ex),
|
|
||||||
_ => ex,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return parameters.Get<int>("NumeroReservado");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int?> GetUltimoNumeroAsync(int puntoDeVentaId, TipoComprobante tipo, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
const string sql = """
|
|
||||||
SELECT UltimoNumero
|
|
||||||
FROM dbo.SecuenciaComprobante
|
|
||||||
WHERE PuntoDeVentaId = @PuntoDeVentaId AND TipoComprobante = @TipoComprobante
|
|
||||||
""";
|
|
||||||
|
|
||||||
await using var connection = _connectionFactory.CreateConnection();
|
|
||||||
await connection.OpenAsync(ct);
|
|
||||||
|
|
||||||
return await connection.QuerySingleOrDefaultAsync<int?>(sql, new
|
|
||||||
{
|
|
||||||
PuntoDeVentaId = puntoDeVentaId,
|
|
||||||
TipoComprobante = (byte)tipo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── mapping ───────────────────────────────────────────────────────────────
|
// ── mapping ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static PuntoDeVenta MapRow(PdvRow r)
|
private static PuntoDeVenta MapRow(PdvRow r)
|
||||||
|
|||||||
Reference in New Issue
Block a user