unit frm_ClientesGruposPrecios;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Math, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.Grids, Vcl.DBGrids,
  Vcl.ExtCtrls, Data.DB, FireDAC.Comp.Client, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Param, FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
  FireDAC.Stan.Async, FireDAC.DApt, uAppTheme, uBaseForm, FireDAC.Comp.DataSet;

type
  TfrmClientesGruposPrecios = class(TForm)
    pnlBottom: TPanel;
    btnCerrar: TBitBtn;
    pnlTop: TPanel;
    lblGrupo: TLabel;
    lblGrupoNombre: TLabel;
    pnlMain: TPanel;
    dbgPrecios: TDBGrid;
    pnlRight: TPanel;
    btnAnadir: TSpeedButton;
    btnEliminar: TSpeedButton;
    btnAyudaRight: TSpeedButton;
    btnAyuda: TBitBtn;
    dsPrecios: TDataSource;
    qryPrecios: TFDQuery;
    procedure FormShow(Sender: TObject);
    procedure btnCerrarClick(Sender: TObject);
    procedure btnAyudaClick(Sender: TObject);
    procedure btnAnadirClick(Sender: TObject);
    procedure btnEliminarClick(Sender: TObject);
    procedure qryPreciosCalcFields(DataSet: TDataSet);
    procedure DateFieldSetText(Sender: TField; const Text: string);
    procedure PrecioBaseChange(Sender: TField);
    procedure PrecioConIvaChange(Sender: TField);
    procedure DescuentoPorcentajeChange(Sender: TField);
    procedure dbgPreciosKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure qryPreciosAfterPost(DataSet: TDataSet);
    procedure qryPreciosUpdateRecord(ASender: TDataSet; ARequest: TFDUpdateRequest;
      var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
  private
    FGrupoId: string;
    FGrupoNombre: string;
    FUpdatingCalcField: Boolean;
    procedure LoadPrecios;
  public
    property GrupoId: string read FGrupoId write FGrupoId;
    property GrupoNombre: string read FGrupoNombre write FGrupoNombre;
  end;

var
  frmClientesGruposPrecios: TfrmClientesGruposPrecios;

implementation

uses
  dmg_Main, uDbErrorHandler, frm_SelectProduct, System.UITypes;

{$R *.dfm}

procedure TfrmClientesGruposPrecios.FormShow(Sender: TObject);
begin
  TAppTheme.ApplyToForm(Self);
  lblGrupoNombre.Caption := FGrupoNombre;

  qryPrecios.Close;
  qryPrecios.Connection := dmgMain.dbConn;
  qryPrecios.CachedUpdates := True;
  qryPrecios.OnUpdateRecord := qryPreciosUpdateRecord;
  qryPrecios.AfterPost := qryPreciosAfterPost;

  qryPrecios.SQL.Text :=
    'SELECT cgp.id, cgp.id_cliente_grupo, cgp.id_articulo, cgp.precio, cgp.valido_desde, cgp.valido_hasta, cgp.und_min, cgp.und_max, ' +
    '       a.id as codigo, a.descripcion as articulo_desc, a.pvp1 as tarifa_pvp1, COALESCE(t.tipo, 10.0) as tipo_iva, ' +
    '       ROUND(cgp.precio * (1 + (COALESCE(t.tipo, 10.00) / 100)), 2) as precio_con_iva, ' +
    '       CAST(0.00 AS DECIMAL(12,2)) as descuento_pct ' +
    'FROM ge_clientes_grupos_precios cgp ' +
    'INNER JOIN ge_articulos a ON cgp.id_articulo = a.id ' +
    'LEFT JOIN ge_tipos_iva t ON a.id_tipo_iva = t.id ' +
    'WHERE cgp.id_cliente_grupo = :id_grupo ' +
    'ORDER BY a.descripcion, cgp.und_min, cgp.valido_desde';

  LoadPrecios;

  if qryPrecios.FindField('precio') is TNumericField then
  begin
    TNumericField(qryPrecios.FieldByName('precio')).DisplayFormat := '#,##0.00';
    qryPrecios.FieldByName('precio').OnChange := PrecioBaseChange;
  end;

  if qryPrecios.FindField('und_min') is TNumericField then
    TNumericField(qryPrecios.FieldByName('und_min')).DisplayFormat := '#,##0';

  if qryPrecios.FindField('und_max') is TNumericField then
    TNumericField(qryPrecios.FieldByName('und_max')).DisplayFormat := '#,##0';

  if qryPrecios.FindField('descuento_pct') is TNumericField then
  begin
    TNumericField(qryPrecios.FieldByName('descuento_pct')).DisplayFormat := '0.00 %';
    qryPrecios.FieldByName('descuento_pct').ProviderFlags := [];
    qryPrecios.FieldByName('descuento_pct').ReadOnly := False;
    qryPrecios.FieldByName('descuento_pct').OnChange := DescuentoPorcentajeChange;
  end;

  if qryPrecios.FindField('precio_con_iva') is TNumericField then
  begin
    TNumericField(qryPrecios.FieldByName('precio_con_iva')).DisplayFormat := '#,##0.00';
    qryPrecios.FieldByName('precio_con_iva').ProviderFlags := [];
    qryPrecios.FieldByName('precio_con_iva').ReadOnly := False;
    qryPrecios.FieldByName('precio_con_iva').OnChange := PrecioConIvaChange;
  end;

  if qryPrecios.FindField('valido_desde') is TDateTimeField then
  begin
    TDateTimeField(qryPrecios.FieldByName('valido_desde')).DisplayFormat := 'dd/mm/yyyy';
    TDateTimeField(qryPrecios.FieldByName('valido_desde')).EditMask := '!99/99/9999;1; ';
    TDateTimeField(qryPrecios.FieldByName('valido_desde')).OnSetText := DateFieldSetText;
  end;
  if qryPrecios.FindField('valido_hasta') is TDateTimeField then
  begin
    TDateTimeField(qryPrecios.FieldByName('valido_hasta')).DisplayFormat := 'dd/mm/yyyy';
    TDateTimeField(qryPrecios.FieldByName('valido_hasta')).EditMask := '!99/99/9999;1; ';
    TDateTimeField(qryPrecios.FieldByName('valido_hasta')).OnSetText := DateFieldSetText;
  end;

  dbgPrecios.OnKeyDown := dbgPreciosKeyDown;
  dbgPrecios.Font.Size := 9;
  dbgPrecios.TitleFont.Size := 9;

  dbgPrecios.Columns.Clear;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'codigo';
    Title.Caption := 'Ref. Art.';
    Width := 55;
    ReadOnly := True;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'articulo_desc';
    Title.Caption := 'Artículo';
    Width := 150;
    ReadOnly := True;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'und_min';
    Title.Caption := 'Ud. Mín.';
    Width := 55;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'und_max';
    Title.Caption := 'Ud. Máx.';
    Width := 55;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'tarifa_pvp1';
    Title.Caption := 'Tarifa Base';
    Width := 65;
    ReadOnly := True;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'precio';
    Title.Caption := 'Precio Sin IVA';
    Width := 75;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'descuento_pct';
    Title.Caption := '% Dto.';
    Width := 55;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'precio_con_iva';
    Title.Caption := 'Precio Con IVA';
    Width := 80;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'valido_desde';
    Title.Caption := 'Válido Desde';
    Width := 75;
  end;
  with dbgPrecios.Columns.Add do
  begin
    FieldName := 'valido_hasta';
    Title.Caption := 'Válido Hasta';
    Width := 75;
  end;
end;

procedure TfrmClientesGruposPrecios.qryPreciosCalcFields(DataSet: TDataSet);
var
  LPrecioBase, LTarifa, LTipoIva, LDtoPct, LPrecioConIva: Double;
  FldPrecio, FldTarifa, FldTipoIva, FldPrecioConIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField then Exit;
  FldPrecio := DataSet.FindField('precio');
  FldTarifa := DataSet.FindField('tarifa_pvp1');
  FldTipoIva := DataSet.FindField('tipo_iva');
  FldPrecioConIva := DataSet.FindField('precio_con_iva');
  FldDtoPct := DataSet.FindField('descuento_pct');

  if Assigned(FldPrecio) and Assigned(FldTipoIva) and Assigned(FldPrecioConIva) then
  begin
    LPrecioBase := FldPrecio.AsFloat;
    LTipoIva := FldTipoIva.AsFloat;
    LPrecioConIva := SimpleRoundTo(LPrecioBase * (1 + (LTipoIva / 100)), -2);
    FldPrecioConIva.AsFloat := LPrecioConIva;

    if Assigned(FldTarifa) and Assigned(FldDtoPct) then
    begin
      LTarifa := FldTarifa.AsFloat;
      if LTarifa > 0 then
        LDtoPct := SimpleRoundTo(((LTarifa - LPrecioBase) / LTarifa) * 100, -2)
      else
        LDtoPct := 0;
      FldDtoPct.AsFloat := LDtoPct;
    end;
  end;
end;

procedure TfrmClientesGruposPrecios.DateFieldSetText(Sender: TField; const Text: string);
var
  LCleanText, LDateStr: string;
  LDate: TDateTime;
  LParts: TArray<string>;
  LDay, LMonth, LYear: Integer;
begin
  LCleanText := StringReplace(StringReplace(Text, '/', '', [rfReplaceAll]), ' ', '', [rfReplaceAll]);
  if LCleanText = '' then
  begin
    Sender.Clear;
    Exit;
  end;

  LDateStr := Trim(Text);

  if Pos('/', LDateStr) > 0 then
  begin
    LParts := LDateStr.Split(['/']);
    if Length(LParts) = 3 then
    begin
      LDay := StrToIntDef(Trim(LParts[0]), 0);
      LMonth := StrToIntDef(Trim(LParts[1]), 0);
      LYear := StrToIntDef(Trim(LParts[2]), 0);
      if (LYear > 0) and (LYear < 100) then
        LYear := 2000 + LYear;

      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;
  end
  else
  begin
    if Length(LCleanText) = 6 then
    begin
      LDay := StrToIntDef(Copy(LCleanText, 1, 2), 0);
      LMonth := StrToIntDef(Copy(LCleanText, 3, 2), 0);
      LYear := 2000 + StrToIntDef(Copy(LCleanText, 5, 2), 0);
      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;

    if Length(LCleanText) = 8 then
    begin
      LDay := StrToIntDef(Copy(LCleanText, 1, 2), 0);
      LMonth := StrToIntDef(Copy(LCleanText, 3, 2), 0);
      LYear := StrToIntDef(Copy(LCleanText, 5, 4), 0);
      if (LDay >= 1) and (LDay <= 31) and (LMonth >= 1) and (LMonth <= 12) and (LYear >= 1900) then
      begin
        try
          Sender.AsDateTime := EncodeDate(LYear, LMonth, LDay);
          Exit;
        except
        end;
      end;
    end;
  end;

  if TryStrToDate(LDateStr, LDate) then
    Sender.AsDateTime := LDate
  else
    Sender.Clear;
end;

procedure TfrmClientesGruposPrecios.PrecioBaseChange(Sender: TField);
var
  LPrecioBase, LTarifa, LTipoIva, LPrecioConIvaCalc, LDtoPctCalc: Double;
  FldPrecioConIva, FldTarifa, FldTipoIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField or not (qryPrecios.State in [dsEdit, dsInsert]) then Exit;

  FldPrecioConIva := qryPrecios.FindField('precio_con_iva');
  FldTarifa := qryPrecios.FindField('tarifa_pvp1');
  FldTipoIva := qryPrecios.FindField('tipo_iva');
  FldDtoPct := qryPrecios.FindField('descuento_pct');

  if Assigned(FldPrecioConIva) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LPrecioBase := Sender.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;
      LPrecioConIvaCalc := SimpleRoundTo(LPrecioBase * (1 + (LTipoIva / 100)), -2);
      FldPrecioConIva.AsFloat := LPrecioConIvaCalc;

      if Assigned(FldTarifa) and Assigned(FldDtoPct) then
      begin
        LTarifa := FldTarifa.AsFloat;
        if LTarifa > 0 then
          LDtoPctCalc := SimpleRoundTo(((LTarifa - LPrecioBase) / LTarifa) * 100, -2)
        else
          LDtoPctCalc := 0;
        FldDtoPct.AsFloat := LDtoPctCalc;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;

procedure TfrmClientesGruposPrecios.DescuentoPorcentajeChange(Sender: TField);
var
  LDtoPct, LTarifa, LTipoIva, LPrecioBaseCalc, LPrecioConIvaCalc: Double;
  FldPrecio, FldPrecioConIva, FldTarifa, FldTipoIva: TField;
begin
  if FUpdatingCalcField or not (qryPrecios.State in [dsEdit, dsInsert]) then Exit;

  FldPrecio := qryPrecios.FindField('precio');
  FldPrecioConIva := qryPrecios.FindField('precio_con_iva');
  FldTarifa := qryPrecios.FindField('tarifa_pvp1');
  FldTipoIva := qryPrecios.FindField('tipo_iva');

  if Assigned(FldPrecio) and Assigned(FldTarifa) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LDtoPct := Sender.AsFloat;
      LTarifa := FldTarifa.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;

      LPrecioBaseCalc := SimpleRoundTo(LTarifa * (1 - (LDtoPct / 100)), -2);
      FldPrecio.AsFloat := LPrecioBaseCalc;

      if Assigned(FldPrecioConIva) then
      begin
        LPrecioConIvaCalc := SimpleRoundTo(LPrecioBaseCalc * (1 + (LTipoIva / 100)), -2);
        FldPrecioConIva.AsFloat := LPrecioConIvaCalc;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;

procedure TfrmClientesGruposPrecios.PrecioConIvaChange(Sender: TField);
var
  LPrecioConIva, LTarifa, LTipoIva, LPrecioBaseCalc, LDtoPctCalc: Double;
  FldPrecio, FldTarifa, FldTipoIva, FldDtoPct: TField;
begin
  if FUpdatingCalcField or not (qryPrecios.State in [dsEdit, dsInsert]) then Exit;

  FldPrecio := qryPrecios.FindField('precio');
  FldTarifa := qryPrecios.FindField('tarifa_pvp1');
  FldTipoIva := qryPrecios.FindField('tipo_iva');
  FldDtoPct := qryPrecios.FindField('descuento_pct');

  if Assigned(FldPrecio) and Assigned(FldTipoIva) then
  begin
    FUpdatingCalcField := True;
    try
      LPrecioConIva := Sender.AsFloat;
      LTipoIva := FldTipoIva.AsFloat;
      if (1 + (LTipoIva / 100)) <> 0 then
      begin
        LPrecioBaseCalc := SimpleRoundTo(LPrecioConIva / (1 + (LTipoIva / 100)), -2);
        FldPrecio.AsFloat := LPrecioBaseCalc;

        if Assigned(FldTarifa) and Assigned(FldDtoPct) then
        begin
          LTarifa := FldTarifa.AsFloat;
          if LTarifa > 0 then
            LDtoPctCalc := SimpleRoundTo(((LTarifa - LPrecioBaseCalc) / LTarifa) * 100, -2)
          else
            LDtoPctCalc := 0;
          FldDtoPct.AsFloat := LDtoPctCalc;
        end;
      end;
    finally
      FUpdatingCalcField := False;
    end;
  end;
end;

procedure TfrmClientesGruposPrecios.dbgPreciosKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = VK_RETURN then
  begin
    Key := 0;
    if dbgPrecios.SelectedIndex < dbgPrecios.Columns.Count - 1 then
    begin
      dbgPrecios.SelectedIndex := dbgPrecios.SelectedIndex + 1;
    end
    else
    begin
      if qryPrecios.State in [dsEdit, dsInsert] then
        qryPrecios.Post;
      qryPrecios.Next;
      dbgPrecios.SelectedIndex := 2;
    end;
  end;
end;

procedure TfrmClientesGruposPrecios.LoadPrecios;
var
  LIdInt: Integer;
begin
  qryPrecios.Close;
  if TryStrToInt(FGrupoId, LIdInt) then
    qryPrecios.ParamByName('id_grupo').AsInteger := LIdInt
  else
    qryPrecios.ParamByName('id_grupo').AsString := FGrupoId;
  qryPrecios.Open;
end;

procedure TfrmClientesGruposPrecios.qryPreciosAfterPost(DataSet: TDataSet);
begin
  qryPrecios.ApplyUpdates;
end;

procedure TfrmClientesGruposPrecios.btnCerrarClick(Sender: TObject);
begin
  if qryPrecios.State in [dsEdit, dsInsert] then
    qryPrecios.Post;
  if qryPrecios.ChangeCount > 0 then
    qryPrecios.ApplyUpdates;
  Close;
end;

procedure TfrmClientesGruposPrecios.btnAyudaClick(Sender: TObject);
begin
  MessageDlg('AYUDA: PRECIOS ESPECÍFICOS DE GRUPOS DE CLIENTES / DISTRIBUIDORES' + sLineBreak + sLineBreak +
             '1. EDICIÓN Y CÁLCULO DE PRECIOS:' + sLineBreak +
             '- Ingrese "Precio Sin IVA", "% Dto." o "Precio Con IVA" en la tabla.' + sLineBreak +
             '- Los cálculos entre precio base, porcentaje de descuento sobre tarifa y precio final con IVA se sincronizan automáticamente.' + sLineBreak + sLineBreak +
             '2. RANGOS DE UNIDADES Y PERÍODOS DE VALIDEZ:' + sLineBreak +
             '- "Ud. Mín." y "Ud. Máx.": Indican la cantidad mínima y máxima de unidades para aplicar el precio.' + sLineBreak +
             '  * Ud. Mín vacía y Ud. Máx con valor: Se aplica desde 0 (cualquier unidad) hasta Ud. Máx.' + sLineBreak +
             '  * Ud. Máx vacía y Ud. Mín con valor: Se aplica desde Ud. Mín en adelante sin límite superior.' + sLineBreak +
             '- "Válido Desde" y/o "Válido Hasta": Indique fechas de validez en formato DD/MM/YYYY.' + sLineBreak + sLineBreak +
             '3. NAVEGACIÓN Y TECLADO:' + sLineBreak +
             '- Presione ENTER para avanzar a la siguiente celda o guardar el registro.',
             mtInformation, [mbOK], 0);
end;

procedure TfrmClientesGruposPrecios.btnAnadirClick(Sender: TObject);
var
  i: Integer;
  LArtId: string;
  LPrice: Double;
begin
  if FGrupoId = '' then Exit;

  if not Assigned(frmSelectProduct) then
    Application.CreateForm(TfrmSelectProduct, frmSelectProduct);

  frmSelectProduct.MultiSelect := True;
  try
    if frmSelectProduct.ShowModal = mrOk then
    begin
      for i := 0 to frmSelectProduct.SelectedIds.Count - 1 do
      begin
        LArtId := frmSelectProduct.SelectedIds[i];
        LPrice := StrToFloatDef(frmSelectProduct.SelectedPrices[i], 0);

        dmgMain.qryExec.Close;
        dmgMain.qryExec.SQL.Text :=
          'INSERT INTO ge_clientes_grupos_precios (id_cliente_grupo, id_articulo, precio, id_user_creator, id_user_update) ' +
          'VALUES (:id_grupo, :id_articulo, :precio, :user, :user)';
        dmgMain.qryExec.ParamByName('id_grupo').AsInteger := StrToIntDef(FGrupoId, 0);
        dmgMain.qryExec.ParamByName('id_articulo').AsInteger := StrToIntDef(LArtId, 0);
        dmgMain.qryExec.ParamByName('precio').AsFloat := LPrice;
        dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
        dmgMain.qryExec.ExecSQL;
        dmgMain.qryExec.Close;
      end;

      LoadPrecios;
    end;
  finally
    frmSelectProduct.MultiSelect := False;
  end;
end;

procedure TfrmClientesGruposPrecios.btnEliminarClick(Sender: TObject);
begin
  if qryPrecios.IsEmpty then Exit;

  if MessageDlg('¿Desea eliminar el precio específico seleccionado?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_clientes_grupos_precios WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsInteger := qryPrecios.FieldByName('id').AsInteger;
    dmgMain.qryExec.ExecSQL;
    dmgMain.qryExec.Close;
    LoadPrecios;
  end;
end;

procedure TfrmClientesGruposPrecios.qryPreciosUpdateRecord(ASender: TDataSet; ARequest: TFDUpdateRequest;
  var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
var
  LUndMin, LUndMax: Integer;
  LValidoDesde, LValidoHasta: TDateTime;
  LHasDesde, LHasHasta: Boolean;
  LArticuloId, LRecordId: Integer;
  LQryCheck: TFDQuery;
begin
  if ARequest = arUpdate then
  begin
    // Validar rango de unidades
    if not ASender.FieldByName('und_min').IsNull and not ASender.FieldByName('und_max').IsNull then
    begin
      if ASender.FieldByName('und_min').AsInteger > ASender.FieldByName('und_max').AsInteger then
      begin
        AAction := eaFail;
        ShowMessage('Error: La unidad mínima (' + ASender.FieldByName('und_min').AsString +
                    ') no puede ser mayor que la unidad máxima (' + ASender.FieldByName('und_max').AsString + ').');
        Exit;
      end;
    end;

    // Comprobar solapamiento de rangos en base de datos
    LRecordId := ASender.FieldByName('id').AsInteger;
    LArticuloId := ASender.FieldByName('id_articulo').AsInteger;
    
    if ASender.FieldByName('und_min').IsNull then LUndMin := 0 else LUndMin := ASender.FieldByName('und_min').AsInteger;
    if ASender.FieldByName('und_max').IsNull then LUndMax := 2147483647 else LUndMax := ASender.FieldByName('und_max').AsInteger;

    LHasDesde := not ASender.FieldByName('valido_desde').IsNull;
    LHasHasta := not ASender.FieldByName('valido_hasta').IsNull;
    if LHasDesde then LValidoDesde := ASender.FieldByName('valido_desde').AsDateTime else LValidoDesde := 0;
    if LHasHasta then LValidoHasta := ASender.FieldByName('valido_hasta').AsDateTime else LValidoHasta := 0;

    LQryCheck := TFDQuery.Create(nil);
    try
      LQryCheck.Connection := dmgMain.dbConn;
      LQryCheck.SQL.Text :=
        'SELECT id, und_min, und_max, valido_desde, valido_hasta FROM ge_clientes_grupos_precios ' +
        'WHERE id_cliente_grupo = :id_grupo AND id_articulo = :id_art AND id <> :id';
      LQryCheck.ParamByName('id_grupo').AsInteger := StrToIntDef(FGrupoId, 0);
      LQryCheck.ParamByName('id_art').AsInteger := LArticuloId;
      LQryCheck.ParamByName('id').AsInteger := LRecordId;
      LQryCheck.Open;

      while not LQryCheck.Eof do
      begin
        var EMin: Integer; if LQryCheck.FieldByName('und_min').IsNull then EMin := 0 else EMin := LQryCheck.FieldByName('und_min').AsInteger;
        var EMax: Integer; if LQryCheck.FieldByName('und_max').IsNull then EMax := 2147483647 else EMax := LQryCheck.FieldByName('und_max').AsInteger;

        // Si solapan los rangos de unidades
        if (LUndMin <= EMax) and (LUndMax >= EMin) then
        begin
          // Comprobar solapamiento de fechas
          var EHasDesde: Boolean := not LQryCheck.FieldByName('valido_desde').IsNull;
          var EHasHasta: Boolean := not LQryCheck.FieldByName('valido_hasta').IsNull;
          var EDesde: TDateTime; if EHasDesde then EDesde := LQryCheck.FieldByName('valido_desde').AsDateTime else EDesde := 0;
          var EHasta: TDateTime; if EHasHasta then EHasta := LQryCheck.FieldByName('valido_hasta').AsDateTime else EHasta := 0;

          var DatesOverlap: Boolean := True;
          if LHasDesde and EHasHasta and (LValidoDesde > EHasta) then DatesOverlap := False;
          if LHasHasta and EHasDesde and (LValidoHasta < EDesde) then DatesOverlap := False;

          if DatesOverlap then
          begin
            ShowMessage('Aviso: El rango de unidades/fechas introducido coincide o se solapa con otra regla de este artículo.');
            Break;
          end;
        end;
        LQryCheck.Next;
      end;
    finally
      LQryCheck.Free;
    end;

    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text :=
        'UPDATE ge_clientes_grupos_precios SET precio = :precio, und_min = :und_min, und_max = :und_max, ' +
        'valido_desde = :valido_desde, valido_hasta = :valido_hasta, ' +
        'id_user_update = :user, update_at = CURRENT_TIMESTAMP ' +
        'WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsInteger := LRecordId;
      dmgMain.qryExec.ParamByName('precio').AsFloat := ASender.FieldByName('precio').AsFloat;

      dmgMain.qryExec.ParamByName('und_min').DataType := ftInteger;
      if ASender.FieldByName('und_min').IsNull then
        dmgMain.qryExec.ParamByName('und_min').Clear
      else
        dmgMain.qryExec.ParamByName('und_min').AsInteger := ASender.FieldByName('und_min').AsInteger;

      dmgMain.qryExec.ParamByName('und_max').DataType := ftInteger;
      if ASender.FieldByName('und_max').IsNull then
        dmgMain.qryExec.ParamByName('und_max').Clear
      else
        dmgMain.qryExec.ParamByName('und_max').AsInteger := ASender.FieldByName('und_max').AsInteger;

      dmgMain.qryExec.ParamByName('valido_desde').DataType := ftDate;
      if ASender.FieldByName('valido_desde').IsNull then
        dmgMain.qryExec.ParamByName('valido_desde').Clear
      else
        dmgMain.qryExec.ParamByName('valido_desde').AsDate := ASender.FieldByName('valido_desde').AsDateTime;

      dmgMain.qryExec.ParamByName('valido_hasta').DataType := ftDate;
      if ASender.FieldByName('valido_hasta').IsNull then
        dmgMain.qryExec.ParamByName('valido_hasta').Clear
      else
        dmgMain.qryExec.ParamByName('valido_hasta').AsDate := ASender.FieldByName('valido_hasta').AsDateTime;

      dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
      dmgMain.qryExec.ExecSQL;
      AAction := eaApplied;
    except
      on E: Exception do
        begin
          AAction := eaFail;
          TDbErrorHandler.HandleException(E, 'Error al guardar los cambios del precio específico de grupo');
        end;
    end;
  end
  else if ARequest = arDelete then
  begin
    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_clientes_grupos_precios WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsInteger := ASender.FieldByName('id').AsInteger;
      dmgMain.qryExec.ExecSQL;
      AAction := eaApplied;
    except
      on E: Exception do
        begin
          AAction := eaFail;
          TDbErrorHandler.HandleException(E, 'Error al eliminar el precio específico de grupo');
        end;
    end;
  end
  else
    AAction := eaApplied;
end;

end.
