unit frm_ArticulosEditor;
interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Math, System.UITypes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Buttons,
  Vcl.ComCtrls, Data.DB, FireDAC.Comp.Client, FireDAC.Comp.DataSet, FireDAC.Stan.Intf,
  FireDAC.Stan.Option, uAppTheme, FireDAC.Stan.Param, uBaseForm, Vcl.Grids, Vcl.DBGrids,
  FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
  FireDAC.Stan.Async, FireDAC.DApt;
type
  TfrmArticulosEditor = class(TfrmBase)
    lblCodigo: TLabel;
    edtCodigo: TEdit;
    lblDescripcion: TLabel;
    edtDescripcion: TEdit;
    lblPvp1: TLabel;
    edtPvp1: TEdit;
    lblPvp2: TLabel;
    edtPvp2: TEdit;
    lblPvp3: TLabel;
    edtPvp3: TEdit;
    lblPvp4: TLabel;
    edtPvp4: TEdit;
    lblTipoIva: TLabel;
    cbTipoIva: TComboBox;
    lblFamilia: TLabel;
    cbFamilia: TComboBox;
    lblArticuloMaestro: TLabel;
    cbArticuloMaestro: TComboBox;
    chkActivo: TCheckBox;
    chkIngrediente: TCheckBox;
    dsAlergenos: TDataSource;
    qryAlergenos: TFDQuery;
    pnlImagen: TPanel;
    imgProducto: TImage;
    btnSubirImagen: TSpeedButton;
    btnEliminarImagen: TSpeedButton;
    tsDocumentos: TTabSheet;
    dbgDocumentos: TDBGrid;
    pnlDocActions: TPanel;
    btnSubirDoc: TBitBtn;
    btnVerDoc: TBitBtn;
    btnEliminarDoc: TBitBtn;
    dsDocumentos: TDataSource;
    qryDocumentos: TFDQuery;
    odDocumento: TOpenDialog;
    lblIngredientes: TLabel;
    btnBoldIngredientes: TSpeedButton;
    btnUnderlineIngredientes: TSpeedButton;
    btnSourceIngredientes: TSpeedButton;
    redtIngredientes: TRichEdit;
    btnEans: TButton;
    btnEansDistribuidores: TButton;
    lblEan: TLabel;
    edtEan: TEdit;
    btnFichaTecnica: TButton;
    lblDiasVida: TLabel;
    edtDiasVida: TEdit;
    lblPeso: TLabel;
    edtPeso: TEdit;
    lblCodigoWeb: TLabel;
    edtCodigoWeb: TEdit;
    edtPvp1Iva: TEdit;
    edtPvp2Iva: TEdit;
    edtPvp3Iva: TEdit;
    edtPvp4Iva: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    btnHistoricoTarifas: TBitBtn;
    btnHistoricoPvp: TSpeedButton;
    lblDescripcionEtiqueta: TLabel;
    btnBold: TSpeedButton;
    btnUnderline: TSpeedButton;
    btnSourceDesc: TSpeedButton;
    lblTamano: TLabel;
    cbFontSize: TComboBox;
    redtDescripcionEtiqueta: TRichEdit;
    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnSalirClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure btnAddAlergenoClick(Sender: TObject);
    procedure btnDelAlergenoClick(Sender: TObject);
    procedure qryAlergenosUpdateRecord(ASender: TDataSet;
      ARequest: TFDUpdateRequest; var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
    procedure btnSubirImagenClick(Sender: TObject);
    procedure btnEliminarImagenClick(Sender: TObject);
    procedure btnSubirDocClick(Sender: TObject);
    procedure btnVerDocClick(Sender: TObject);
    procedure btnEliminarDocClick(Sender: TObject);
    procedure btnEansClick(Sender: TObject);
    procedure btnEansDistribuidoresClick(Sender: TObject);
    procedure btnFichaTecnicaClick(Sender: TObject);
    procedure btnHistoricoPvpClick(Sender: TObject);
    procedure btnBoldClick(Sender: TObject);
    procedure btnUnderlineClick(Sender: TObject);
    procedure btnSourceDescClick(Sender: TObject);
    procedure cbFontSizeChange(Sender: TObject);
    procedure redtDescripcionEtiquetaSelectionChange(Sender: TObject);
    procedure btnBoldIngredientesClick(Sender: TObject);
    procedure btnUnderlineIngredientesClick(Sender: TObject);
    procedure btnSourceIngredientesClick(Sender: TObject);
    procedure redtIngredientesSelectionChange(Sender: TObject);
    procedure NumericEditKeyPress(Sender: TObject; var Key: Char);
  protected
    procedure KeyPress(var Key: Char); override;
    procedure DoAnadir; override;
    procedure DoModificar; override;
    procedure DoPrimero; override;
    procedure DoAnterior; override;
    procedure DoSiguiente; override;
    procedure DoUltimo; override;
    function IsNewRecord: Boolean; override;
  private
    FArticuloId: string;
    FIvaIds: TStringList;
    FIvaPorcentajes: TStringList;
    FFamiliaIds: TStringList;
    FArticuloMaestroIds: TStringList;
    FParentForm: TForm;
    FIsNewRecord: Boolean;
    FUpdatingPrices: Boolean;
    procedure LoadData;
    procedure LoadLookups;
    procedure LoadProductImage;
    procedure LoadDocuments;
    function GetSelectedIvaRate: Double;
    procedure RecalcularPrecioConIva(AEdtSinIva, AEdtConIva: TEdit);
    procedure RecalcularPrecioSinIva(AEdtConIva, AEdtSinIva: TEdit);
    procedure RecalcularTodosConIva;
    procedure edtPvpSinIvaChange(Sender: TObject);
    procedure edtPvpConIvaChange(Sender: TObject);
    procedure cbTipoIvaChange(Sender: TObject);
    procedure UpdateFormatToolbarState;
  public
    property ArticuloId: string read FArticuloId write FArticuloId;
    property ParentForm: TForm read FParentForm write FParentForm;
  end;
var
  frmArticulosEditor: TfrmArticulosEditor;
implementation
uses
  dmg_Main, uDbErrorHandler, frm_Articulos, frm_SelectAlergeno, frm_SelectProduct, Winapi.ShellAPI, System.IOUtils,
  Vcl.Imaging.pngimage, Vcl.Imaging.jpeg, Vcl.Imaging.GIFImg, frm_ArticulosEans, frm_ArticulosClientesEan, frm_FichaTecnicaEditor, frm_SelectMateriaPrima,
  frm_VentasPreciosHistorico, uRichEditHtmlHelper, uDbParamsHelper;
{$R *.dfm}



function ParseFloatSeguro(const AText: string): Double;
var
  LText: string;
  LHasComma, LHasDot: Boolean;
begin
  LText := Trim(AText);
  LText := StringReplace(LText, ' ', '', [rfReplaceAll]);
  
  LHasComma := Pos(',', LText) > 0;
  LHasDot := Pos('.', LText) > 0;
  
  if FormatSettings.DecimalSeparator = ',' then
  begin
    if LHasDot and not LHasComma then
      LText := StringReplace(LText, '.', ',', [rfReplaceAll])
    else if LHasDot and LHasComma then
    begin
      if Pos('.', LText) < Pos(',', LText) then
        LText := StringReplace(LText, '.', '', [rfReplaceAll])
      else
        LText := StringReplace(LText, ',', '', [rfReplaceAll]);
    end;
  end
  else
  begin
    if LHasComma and not LHasDot then
      LText := StringReplace(LText, ',', '.', [rfReplaceAll])
    else if LHasComma and LHasDot then
    begin
      if Pos(',', LText) < Pos('.', LText) then
        LText := StringReplace(LText, ',', '', [rfReplaceAll])
      else
        LText := StringReplace(LText, '.', '', [rfReplaceAll]);
    end;
  end;
  
  Result := StrToFloatDef(LText, 0);
end;

procedure TfrmArticulosEditor.NumericEditKeyPress(Sender: TObject; var Key: Char);
begin
  if (Key = '.') and (FormatSettings.DecimalSeparator = ',') then
    Key := ','
  else if (Key = ',') and (FormatSettings.DecimalSeparator = '.') then
    Key := '.';
end;

procedure TfrmArticulosEditor.KeyPress(var Key: Char);
begin
  if CharInSet(Key, ['.', ',']) and Assigned(ActiveControl) then
  begin
    if (ActiveControl = edtPvp1) or (ActiveControl = edtPvp2) or
       (ActiveControl = edtPvp3) or (ActiveControl = edtPvp4) or
       (ActiveControl = edtPvp1Iva) or (ActiveControl = edtPvp2Iva) or
       (ActiveControl = edtPvp3Iva) or (ActiveControl = edtPvp4Iva) or
       (ActiveControl = edtPeso) then
    begin
      if (Key = '.') and (FormatSettings.DecimalSeparator = ',') then
        Key := ','
      else if (Key = ',') and (FormatSettings.DecimalSeparator = '.') then
        Key := '.';
    end;
  end;
  inherited KeyPress(Key);
end;

procedure TfrmArticulosEditor.FormShow(Sender: TObject);
begin
  if not Assigned(FIvaIds) then
    FIvaIds := TStringList.Create
  else
    FIvaIds.Clear;
  if not Assigned(FIvaPorcentajes) then
    FIvaPorcentajes := TStringList.Create
  else
    FIvaPorcentajes.Clear;
  if not Assigned(FFamiliaIds) then
    FFamiliaIds := TStringList.Create
  else
    FFamiliaIds.Clear;
  
  FUpdatingPrices := False;
  // Aplicar tema estándar
  TAppTheme.ApplyToForm(Self);
  edtCodigo.ReadOnly := True;
  // Vincular eventos a la barra superior heredada
  btnGuardar.OnClick := btnSaveClick;
  btnSalir.OnClick := btnSalirClick;
  btnNuevoArticulo.OnClick := btnAddAlergenoClick;
  btnEliminaArticulo.OnClick := btnDelAlergenoClick;
  // Vincular la grilla de alérgenos
  dbgItems.DataSource := dsAlergenos;
  // Mostrar componentes heredados para la grilla y botones de alérgenos
  pgcDetails.Visible := not FIsNewRecord;
  TsListado.TabVisible := False;
  pgcDetails.ActivePage := tsDatosEnvio;
  pnlSideToolbar.Visible := True;
  dbgItems.Visible := True;
  pnlFooter.Visible := False;
  
  // Configurar las columnas de la grilla de alérgenos
  dbgItems.Columns.Clear;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'id_alergeno';
    Title.Caption := 'ID';
    Width := 80;
    ReadOnly := True;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'nombre';
    Title.Caption := 'Alérgeno';
    Width := 220;
    ReadOnly := True;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'nombre_tecnico';
    Title.Caption := 'Nombre Técnico';
    Width := 250;
    ReadOnly := True;
  end;
  with dbgItems.Columns.Add do
  begin
    FieldName := 'presencia';
    Title.Caption := 'Presencia';
    Width := 100;
    PickList.Clear;
    PickList.Add('Contiene');
    PickList.Add('Trazas');
    PickList.Add('No contiene');
  end;

  // Configurar las columnas de la grilla de documentos
  dbgDocumentos.DataSource := dsDocumentos;
  dbgDocumentos.Columns.Clear;
  with dbgDocumentos.Columns.Add do
  begin
    FieldName := 'fecha';
    Title.Caption := 'Fecha';
    Width := 90;
    ReadOnly := True;
  end;
  with dbgDocumentos.Columns.Add do
  begin
    FieldName := 'descripcion';
    Title.Caption := 'Descripción';
    Width := 200;
    ReadOnly := True;
  end;
  with dbgDocumentos.Columns.Add do
  begin
    FieldName := 'nombre_archivo';
    Title.Caption := 'Archivo';
    Width := 250;
    ReadOnly := True;
  end;
  with dbgDocumentos.Columns.Add do
  begin
    FieldName := 'extension';
    Title.Caption := 'Ext.';
    Width := 60;
    ReadOnly := True;
  end;

  
  // Hookear el evento de actualización de FireDAC para que grabe directamente
  qryAlergenos.OnUpdateRecord := qryAlergenosUpdateRecord;
  // Establecer la consulta SQL de manera explícita para forzar a FireDAC a crear los parámetros
  qryAlergenos.SQL.Text :=
    'SELECT aa.id, aa.id_articulo, aa.id_alergeno, aa.presencia, aa.calculado_receta, ' +
    '  a.nombre, a.nombre_tecnico ' +
    'FROM ge_articulos_alergenos aa ' +
    'INNER JOIN ge_alergenos a ON aa.id_alergeno = a.id ' +
    'WHERE aa.id_articulo = :id_articulo';
  qryDocumentos.SQL.Text :=
    'SELECT id, fecha, descripcion, nombre_archivo, extension ' +
    'FROM ge_documentos ' +
    'WHERE id_articulo = :id_articulo';
  // Cargar tablas maestras (IVA y Familias)
  LoadLookups;

  // Vincular eventos de cambio para los edits de precio
  edtPvp1.OnChange := edtPvpSinIvaChange;
  edtPvp2.OnChange := edtPvpSinIvaChange;
  edtPvp3.OnChange := edtPvpSinIvaChange;
  edtPvp4.OnChange := edtPvpSinIvaChange;

  edtPvp1Iva.OnChange := edtPvpConIvaChange;
  edtPvp2Iva.OnChange := edtPvpConIvaChange;
  edtPvp3Iva.OnChange := edtPvpConIvaChange;
  edtPvp4Iva.OnChange := edtPvpConIvaChange;

  edtPvp1.OnKeyPress := NumericEditKeyPress;
  edtPvp2.OnKeyPress := NumericEditKeyPress;
  edtPvp3.OnKeyPress := NumericEditKeyPress;
  edtPvp4.OnKeyPress := NumericEditKeyPress;
  edtPvp1Iva.OnKeyPress := NumericEditKeyPress;
  edtPvp2Iva.OnKeyPress := NumericEditKeyPress;
  edtPvp3Iva.OnKeyPress := NumericEditKeyPress;
  edtPvp4Iva.OnKeyPress := NumericEditKeyPress;
  edtPeso.OnKeyPress := NumericEditKeyPress;

  cbTipoIva.OnChange := cbTipoIvaChange;

  // Configurar desplegable de tamaño de fuente (5 a 30)
  cbFontSize.Items.Clear;
  for var i := 5 to 30 do
    cbFontSize.Items.Add(IntToStr(i));
  cbFontSize.ItemIndex := cbFontSize.Items.IndexOf(IntToStr(redtDescripcionEtiqueta.Font.Size));
  if cbFontSize.ItemIndex < 0 then
    cbFontSize.ItemIndex := cbFontSize.Items.IndexOf('10');

  btnBold.Tag := 999;
  btnUnderline.Tag := 999;
  lblTamano.Tag := 999;
  cbFontSize.Tag := 999;
  btnBoldIngredientes.Tag := 999;
  btnUnderlineIngredientes.Tag := 999;

  FIsNewRecord := (FArticuloId = '');
  // Cargar datos
  if not FIsNewRecord then
    LoadData
  else
  begin
    edtCodigo.Text := '';
    edtDescripcion.Text := '';
    redtDescripcionEtiqueta.Clear;
    edtEan.Text := '';
    edtDiasVida.Text := '';
    edtPeso.Text := '';
    edtCodigoWeb.Text := '';
    redtIngredientes.Clear;
    edtPvp1.Text := '0,00';
    edtPvp2.Text := '0,00';
    edtPvp3.Text := '0,00';
    edtPvp4.Text := '0,00';
    chkActivo.Checked := True;
    chkIngrediente.Checked := False;
    
    if cbTipoIva.Items.Count > 0 then
      cbTipoIva.ItemIndex := 0
    else
      cbTipoIva.ItemIndex := -1;
      
    cbFamilia.ItemIndex := -1;
    cbArticuloMaestro.ItemIndex := 0;
    imgProducto.Picture := nil;
    
    // Abrir query de alérgenos vacía
    qryAlergenos.Close;
    qryAlergenos.ParamByName('id_articulo').AsString := FArticuloId;
    qryAlergenos.Open;

    // Abrir query de documentos vacía
    qryDocumentos.Close;
    qryDocumentos.ParamByName('id_articulo').AsString := FArticuloId;
    qryDocumentos.Open;
  end;
  btnEans.Enabled := not FIsNewRecord;
  btnEansDistribuidores.Enabled := not FIsNewRecord;
  btnFichaTecnica.Enabled := not FIsNewRecord;
  ResetChangeTracking;
end;

procedure TfrmArticulosEditor.btnEansClick(Sender: TObject);
begin
  if FArticuloId = '' then Exit;
  
  if not Assigned(frmArticulosEans) then
    Application.CreateForm(TfrmArticulosEans, frmArticulosEans);
    
  frmArticulosEans.ArticuloId := StrToIntDef(FArticuloId, 0);
  frmArticulosEans.ArticuloDesc := edtDescripcion.Text;
  frmArticulosEans.ShowModal;
end;

procedure TfrmArticulosEditor.btnEansDistribuidoresClick(Sender: TObject);
begin
  if FArticuloId = '' then Exit;
  
  if not Assigned(frmArticulosClientesEan) then
    Application.CreateForm(TfrmArticulosClientesEan, frmArticulosClientesEan);
    
  frmArticulosClientesEan.ArticuloId := StrToIntDef(FArticuloId, 0);
  frmArticulosClientesEan.ArticuloDesc := edtDescripcion.Text;
  frmArticulosClientesEan.ArticuloEan := edtEan.Text;
  frmArticulosClientesEan.ArticuloDescEtiqueta := GetRichEditHtml(redtDescripcionEtiqueta, btnSourceDesc);
  frmArticulosClientesEan.ArticuloIngredientes := GetRichEditHtml(redtIngredientes, btnSourceIngredientes);
  frmArticulosClientesEan.ShowModal;
end;

procedure TfrmArticulosEditor.btnFichaTecnicaClick(Sender: TObject);
begin
  if FArticuloId = '' then Exit;
  
  if not Assigned(frmFichaTecnicaEditor) then
    Application.CreateForm(TfrmFichaTecnicaEditor, frmFichaTecnicaEditor);
    
  frmFichaTecnicaEditor.ArticuloId := StrToIntDef(FArticuloId, 0);
  frmFichaTecnicaEditor.ShowModal;
end;

procedure TfrmArticulosEditor.btnHistoricoPvpClick(Sender: TObject);
var
  LArticuloId: Integer;
  LTitulo: string;
begin
  LArticuloId := StrToIntDef(Trim(edtCodigo.Text), 0);
  if LArticuloId <= 0 then
    LArticuloId := StrToIntDef(Trim(FArticuloId), 0);

  if LArticuloId <= 0 then
  begin
    ShowMessage('Guarde primero el artículo para consultar su histórico de precios.');
    Exit;
  end;

  LTitulo := 'Histórico de Precios: Art. #' + IntToStr(LArticuloId) + ' - ' + edtDescripcion.Text;
  TfrmVentasPreciosHistorico.MostrarHistorico(Self, '', LArticuloId, 0, 0, LTitulo);
end;

procedure TfrmArticulosEditor.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FIvaIds);
  FreeAndNil(FIvaPorcentajes);
  FreeAndNil(FFamiliaIds);
  FreeAndNil(FArticuloMaestroIds);
end;
procedure TfrmArticulosEditor.LoadLookups;
begin
  if not Assigned(FIvaIds) then
    FIvaIds := TStringList.Create;
  FIvaIds.Clear;

  if not Assigned(FIvaPorcentajes) then
    FIvaPorcentajes := TStringList.Create;
  FIvaPorcentajes.Clear;

  if not Assigned(FFamiliaIds) then
    FFamiliaIds := TStringList.Create;
  FFamiliaIds.Clear;

  if not Assigned(FArticuloMaestroIds) then
    FArticuloMaestroIds := TStringList.Create;
  FArticuloMaestroIds.Clear;

  cbTipoIva.Items.Clear;
  cbFamilia.Items.Clear;
  cbArticuloMaestro.Items.Clear;

  // IVA
  dmgMain.qryExec.Close;
  if FArticuloId <> '' then
  begin
    dmgMain.qryExec.SQL.Text :=
      'SELECT id, descripcion, tipo FROM ge_tipos_iva ' +
      'WHERE (activo = 1 OR activo IS NULL) OR (id = (SELECT id_tipo_iva FROM ge_articulos WHERE id = :art_id LIMIT 1)) ' +
      'ORDER BY tipo';
    dmgMain.qryExec.ParamByName('art_id').AsString := FArticuloId;
  end
  else
  begin
    dmgMain.qryExec.SQL.Text :=
      'SELECT id, descripcion, tipo FROM ge_tipos_iva ' +
      'WHERE (activo = 1 OR activo IS NULL) ' +
      'ORDER BY tipo';
  end;
  dmgMain.qryExec.Open;
  while not dmgMain.qryExec.Eof do
  begin
    cbTipoIva.Items.Add(dmgMain.qryExec.FieldByName('descripcion').AsString + ' (' + FormatFloat('0', dmgMain.qryExec.FieldByName('tipo').AsFloat) + '%)');
    FIvaIds.Add(dmgMain.qryExec.FieldByName('id').AsString);
    FIvaPorcentajes.Add(dmgMain.qryExec.FieldByName('tipo').AsString);
    dmgMain.qryExec.Next;
  end;
  dmgMain.qryExec.Close;

  // Familias
  dmgMain.qryExec.Close;
  if FArticuloId <> '' then
  begin
    dmgMain.qryExec.SQL.Text :=
      'SELECT id, descripcion FROM ge_familias ' +
      'WHERE (activo = 1 OR activo IS NULL) OR (id = (SELECT id_familia FROM ge_articulos WHERE id = :art_id LIMIT 1)) ' +
      'ORDER BY descripcion';
    dmgMain.qryExec.ParamByName('art_id').AsString := FArticuloId;
  end
  else
  begin
    dmgMain.qryExec.SQL.Text :=
      'SELECT id, descripcion FROM ge_familias ' +
      'WHERE (activo = 1 OR activo IS NULL) ' +
      'ORDER BY descripcion';
  end;
  dmgMain.qryExec.Open;
  while not dmgMain.qryExec.Eof do
  begin
    cbFamilia.Items.Add(dmgMain.qryExec.FieldByName('descripcion').AsString);
    FFamiliaIds.Add(dmgMain.qryExec.FieldByName('id').AsString);
    dmgMain.qryExec.Next;
  end;
  dmgMain.qryExec.Close;

  // Artículos Maestros (para trazabilidad y stock unificado)
  cbArticuloMaestro.Items.Add('(Ninguno - Es Artículo Maestro)');
  FArticuloMaestroIds.Add('0');

  dmgMain.qryExec.Close;
  dmgMain.qryExec.SQL.Text :=
    'SELECT id, descripcion FROM ge_articulos ' +
    'WHERE (activo = 1 OR activo IS NULL) ';
  if FArticuloId <> '' then
  begin
    dmgMain.qryExec.SQL.Add('  AND id <> :curr_art_id ');
    dmgMain.qryExec.ParamByName('curr_art_id').AsString := FArticuloId;
  end;
  dmgMain.qryExec.SQL.Add('ORDER BY descripcion');
  dmgMain.qryExec.Open;
  while not dmgMain.qryExec.Eof do
  begin
    cbArticuloMaestro.Items.Add(dmgMain.qryExec.FieldByName('id').AsString + ' - ' + dmgMain.qryExec.FieldByName('descripcion').AsString);
    FArticuloMaestroIds.Add(dmgMain.qryExec.FieldByName('id').AsString);
    dmgMain.qryExec.Next;
  end;
  dmgMain.qryExec.Close;
end;

procedure TfrmArticulosEditor.LoadData;
var
  LIvaId, LFamId: string;
  LIdxIva, LIdxFam: Integer;
begin
  FIsNewRecord := False;
  FUpdatingPrices := True;
  try
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 'SELECT * FROM ge_articulos WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FArticuloId;
    dmgMain.qryExec.Open;
    if not dmgMain.qryExec.IsEmpty then
    begin
      edtCodigo.Text := dmgMain.qryExec.FieldByName('id').AsString;
      
      edtDescripcion.Text := dmgMain.qryExec.FieldByName('descripcion').AsString;
      if (dmgMain.qryExec.FindField('descripcion_etiqueta') <> nil) and not dmgMain.qryExec.FieldByName('descripcion_etiqueta').IsNull then
        LoadRichEditHtml(dmgMain.qryExec.FieldByName('descripcion_etiqueta').AsString, redtDescripcionEtiqueta, btnSourceDesc)
      else
      begin
        if Assigned(btnSourceDesc) then btnSourceDesc.Down := False;
        redtDescripcionEtiqueta.Clear;
      end;
      edtPvp1.Text := FormatFloat('#,##0.00', dmgMain.qryExec.FieldByName('pvp1').AsFloat);
      edtPvp2.Text := FormatFloat('#,##0.00', dmgMain.qryExec.FieldByName('pvp2').AsFloat);
      edtPvp3.Text := FormatFloat('#,##0.00', dmgMain.qryExec.FieldByName('pvp3').AsFloat);
      edtPvp4.Text := FormatFloat('#,##0.00', dmgMain.qryExec.FieldByName('pvp4').AsFloat);
      
      chkActivo.Checked := dmgMain.qryExec.FieldByName('activo').AsInteger <> 0;
      chkIngrediente.Checked := dmgMain.qryExec.FieldByName('ingrediente').AsInteger <> 0;
      
      // Seleccionar IVA en Combo
      LIvaId := dmgMain.qryExec.FieldByName('id_tipo_iva').AsString;
      LIdxIva := FIvaIds.IndexOf(LIvaId);
      if LIdxIva >= 0 then
        cbTipoIva.ItemIndex := LIdxIva
      else if cbTipoIva.Items.Count > 0 then
        cbTipoIva.ItemIndex := 0
      else
        cbTipoIva.ItemIndex := -1;
      
      // Seleccionar Familia en Combo
      if not dmgMain.qryExec.FieldByName('id_familia').IsNull then
      begin
        LFamId := dmgMain.qryExec.FieldByName('id_familia').AsString;
        LIdxFam := FFamiliaIds.IndexOf(LFamId);
        cbFamilia.ItemIndex := LIdxFam;
      end
      else
        cbFamilia.ItemIndex := -1;

      // Seleccionar Artículo Maestro en Combo
      if (dmgMain.qryExec.FindField('id_maestro') <> nil) and not dmgMain.qryExec.FieldByName('id_maestro').IsNull then
      begin
        var LMaestroId := dmgMain.qryExec.FieldByName('id_maestro').AsString;
        var LIdxMaestro := FArticuloMaestroIds.IndexOf(LMaestroId);
        if LIdxMaestro >= 0 then
          cbArticuloMaestro.ItemIndex := LIdxMaestro
        else
          cbArticuloMaestro.ItemIndex := 0;
      end
      else
        cbArticuloMaestro.ItemIndex := 0;
        
      edtEan.Text := dmgMain.qryExec.FieldByName('ean').AsString;
      if not dmgMain.qryExec.FieldByName('dias_vida').IsNull then
        edtDiasVida.Text := dmgMain.qryExec.FieldByName('dias_vida').AsString
      else
        edtDiasVida.Text := '';
        
      if not dmgMain.qryExec.FieldByName('peso').IsNull then
        edtPeso.Text := FormatFloat('#,##0.000', dmgMain.qryExec.FieldByName('peso').AsFloat)
      else
        edtPeso.Text := '';
        
      if dmgMain.qryExec.FindField('codigo_web') <> nil then
        edtCodigoWeb.Text := dmgMain.qryExec.FieldByName('codigo_web').AsString
      else
        edtCodigoWeb.Text := '';

      if (dmgMain.qryExec.FindField('ingredientes') <> nil) and not dmgMain.qryExec.FieldByName('ingredientes').IsNull then
        LoadRichEditHtml(dmgMain.qryExec.FieldByName('ingredientes').AsString, redtIngredientes, btnSourceIngredientes)
      else
      begin
        if Assigned(btnSourceIngredientes) then btnSourceIngredientes.Down := False;
        redtIngredientes.Clear;
      end;
    end;
    dmgMain.qryExec.Close;
  finally
    FUpdatingPrices := False;
  end;
  // Cargar alérgenos
  qryAlergenos.Close;
  qryAlergenos.ParamByName('id_articulo').AsString := FArticuloId;
  qryAlergenos.Open;
  // Cargar imagen de producto
  LoadProductImage;
  // Cargar documentos
  LoadDocuments;
  
  btnEans.Enabled := True;
  btnFichaTecnica.Enabled := True;
  RecalcularTodosConIva;
  ResetChangeTracking;
end;

procedure TfrmArticulosEditor.btnSalirClick(Sender: TObject);
begin
  ModalResult := mrCancel;
  Close;
end;

procedure TfrmArticulosEditor.btnSaveClick(Sender: TObject);
var
  LIsNew: Boolean;
begin
  if Trim(edtDescripcion.Text) = '' then
  begin
    ShowMessage('La descripción del artículo es obligatoria.');
    Exit;
  end;
  if (cbTipoIva.ItemIndex < 0) or (cbTipoIva.ItemIndex >= FIvaIds.Count) then
  begin
    ShowMessage('Debe seleccionar un tipo de IVA.');
    Exit;
  end;
  LIsNew := FIsNewRecord;
  dmgMain.qryExec.Close;
  if LIsNew then
  begin
    dmgMain.qryExec.SQL.Text :=
      'INSERT INTO ge_articulos (descripcion, descripcion_etiqueta, pvp1, pvp2, pvp3, pvp4, ' +
      '  id_tipo_iva, id_familia, id_maestro, activo, ingrediente, ean, dias_vida, peso, codigo_web, ingredientes, id_user_creator, id_user_update) ' +
      'VALUES (:desc, :desc_etiq, :pvp1, :pvp2, :pvp3, :pvp4, :iva, :fam, :id_maestro, :act, :ing, :ean, :dias_vida, :peso, :codweb, :ingredientes, :user, :user)';
  end
  else
  begin
    dmgMain.qryExec.SQL.Text :=
      'UPDATE ge_articulos SET descripcion = :desc, descripcion_etiqueta = :desc_etiq, pvp1 = :pvp1, ' +
      '  pvp2 = :pvp2, pvp3 = :pvp3, pvp4 = :pvp4, id_tipo_iva = :iva, id_familia = :fam, id_maestro = :id_maestro, ' +
      '  activo = :act, ingrediente = :ing, ean = :ean, dias_vida = :dias_vida, peso = :peso, codigo_web = :codweb, ingredientes = :ingredientes, id_user_update = :user, update_at = CURRENT_TIMESTAMP ' +
      'WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := FArticuloId;
  end;
  SetParamString(dmgMain.qryExec.ParamByName('desc'), edtDescripcion.Text);
  SetParamMemo(dmgMain.qryExec.ParamByName('desc_etiq'), GetRichEditHtml(redtDescripcionEtiqueta, btnSourceDesc));
  SetParamMemo(dmgMain.qryExec.ParamByName('ingredientes'), GetRichEditHtml(redtIngredientes, btnSourceIngredientes));
  dmgMain.qryExec.ParamByName('pvp1').AsFloat := ParseFloatSeguro(edtPvp1.Text);
  dmgMain.qryExec.ParamByName('pvp2').AsFloat := ParseFloatSeguro(edtPvp2.Text);
  dmgMain.qryExec.ParamByName('pvp3').AsFloat := ParseFloatSeguro(edtPvp3.Text);
  dmgMain.qryExec.ParamByName('pvp4').AsFloat := ParseFloatSeguro(edtPvp4.Text);
  dmgMain.qryExec.ParamByName('iva').AsString := FIvaIds[cbTipoIva.ItemIndex];
  
  dmgMain.qryExec.ParamByName('fam').DataType := ftString;
  if (cbFamilia.ItemIndex >= 0) and (cbFamilia.ItemIndex < FFamiliaIds.Count) then
    dmgMain.qryExec.ParamByName('fam').AsString := FFamiliaIds[cbFamilia.ItemIndex]
  else
    dmgMain.qryExec.ParamByName('fam').Clear;

  dmgMain.qryExec.ParamByName('id_maestro').DataType := ftInteger;
  if (cbArticuloMaestro.ItemIndex > 0) and (cbArticuloMaestro.ItemIndex < FArticuloMaestroIds.Count) then
  begin
    var LMid := StrToIntDef(FArticuloMaestroIds[cbArticuloMaestro.ItemIndex], 0);
    if LMid > 0 then
      dmgMain.qryExec.ParamByName('id_maestro').AsInteger := LMid
    else
      dmgMain.qryExec.ParamByName('id_maestro').Clear;
  end
  else
    dmgMain.qryExec.ParamByName('id_maestro').Clear;
     
  dmgMain.qryExec.ParamByName('act').AsBoolean := chkActivo.Checked;
  dmgMain.qryExec.ParamByName('ing').AsBoolean := chkIngrediente.Checked;
  dmgMain.qryExec.ParamByName('ean').AsString := edtEan.Text;
  
  dmgMain.qryExec.ParamByName('codweb').DataType := ftString;
  if Trim(edtCodigoWeb.Text) <> '' then
    dmgMain.qryExec.ParamByName('codweb').AsString := Trim(edtCodigoWeb.Text)
  else
    dmgMain.qryExec.ParamByName('codweb').Clear;
  
  dmgMain.qryExec.ParamByName('dias_vida').DataType := ftInteger;
  if Trim(edtDiasVida.Text) <> '' then
    dmgMain.qryExec.ParamByName('dias_vida').AsInteger := StrToIntDef(edtDiasVida.Text, 0)
  else
    dmgMain.qryExec.ParamByName('dias_vida').Clear;
    
  dmgMain.qryExec.ParamByName('peso').DataType := ftFloat;
  if Trim(edtPeso.Text) <> '' then
    dmgMain.qryExec.ParamByName('peso').AsFloat := ParseFloatSeguro(edtPeso.Text)
  else
    dmgMain.qryExec.ParamByName('peso').Clear;
    
  dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
  try
    dmgMain.qryExec.ExecSQL;
    if LIsNew then
    begin
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'SELECT LAST_INSERT_ID()';
      dmgMain.qryExec.Open;
      FArticuloId := dmgMain.qryExec.Fields[0].AsString;
      dmgMain.qryExec.Close;
    end;
    FIsNewRecord := False;
    btnEans.Enabled := True;
    btnFichaTecnica.Enabled := True;
    ResetChangeTracking;
    ShowMessage('Artículo guardado correctamente.');
  except
    on E: Exception do
      TDbErrorHandler.HandleException(E, dmgMain.qryExec.SQL.Text);
  end;
end;
procedure TfrmArticulosEditor.DoAnadir;
begin
  FIsNewRecord := True;
  FArticuloId := '';
  LoadLookups;
  edtCodigo.Text := 'AUTO';
  edtDescripcion.Text := '';
  edtEan.Text := '';
  edtDiasVida.Text := '';
  edtCodigoWeb.Text := '';
  edtPvp1.Text := '0,00';
  edtPvp2.Text := '0,00';
  edtPvp3.Text := '0,00';
  edtPvp4.Text := '0,00';
  chkActivo.Checked := True;
  chkIngrediente.Checked := False;
  if cbTipoIva.Items.Count > 0 then
    cbTipoIva.ItemIndex := 0
  else
    cbTipoIva.ItemIndex := -1;
  cbFamilia.ItemIndex := -1;
  cbArticuloMaestro.ItemIndex := 0;
  // Ocultar query/grid de alérgenos temporalmente al crear nuevo
  pgcDetails.Visible := False;
  imgProducto.Picture := nil;
  btnEans.Enabled := False;
  btnFichaTecnica.Enabled := False;
  qryAlergenos.Close;
  qryDocumentos.Close;
  RecalcularTodosConIva;
  ResetChangeTracking;
  edtDescripcion.SetFocus;
end;
procedure TfrmArticulosEditor.DoModificar;
begin
  edtDescripcion.SetFocus;
end;
procedure TfrmArticulosEditor.DoPrimero;
var
  LParent: TfrmArticulos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmArticulos) then
    LParent := TfrmArticulos(FParentForm)
  else if Assigned(frmArticulos) then
    LParent := frmArticulos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.First;
    FArticuloId := LParent.QryMain.FieldByName('id').AsString;
    LoadLookups;
    LoadData;
  end;
end;
procedure TfrmArticulosEditor.DoAnterior;
var
  LParent: TfrmArticulos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmArticulos) then
    LParent := TfrmArticulos(FParentForm)
  else if Assigned(frmArticulos) then
    LParent := frmArticulos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Prior;
    FArticuloId := LParent.QryMain.FieldByName('id').AsString;
    LoadLookups;
    LoadData;
  end;
end;
procedure TfrmArticulosEditor.DoSiguiente;
var
  LParent: TfrmArticulos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmArticulos) then
    LParent := TfrmArticulos(FParentForm)
  else if Assigned(frmArticulos) then
    LParent := frmArticulos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Next;
    FArticuloId := LParent.QryMain.FieldByName('id').AsString;
    LoadLookups;
    LoadData;
  end;
end;
procedure TfrmArticulosEditor.DoUltimo;
var
  LParent: TfrmArticulos;
begin
  LParent := nil;
  if Assigned(FParentForm) and (FParentForm is TfrmArticulos) then
    LParent := TfrmArticulos(FParentForm)
  else if Assigned(frmArticulos) then
    LParent := frmArticulos;
  if Assigned(LParent) and Assigned(LParent.QryMain) and not LParent.QryMain.IsEmpty then
  begin
    LParent.QryMain.Last;
    FArticuloId := LParent.QryMain.FieldByName('id').AsString;
    LoadLookups;
    LoadData;
  end;
end;
function TfrmArticulosEditor.IsNewRecord: Boolean;
begin
  Result := FIsNewRecord;
end;
procedure TfrmArticulosEditor.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Si cancelamos un registro nuevo, limpiamos los alérgenos e ingredientes temporales
  if (ModalResult <> mrOk) and FIsNewRecord and (FArticuloId <> '') then
  begin
    try
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_articulos_alergenos WHERE id_articulo = :id_art';
      dmgMain.qryExec.ParamByName('id_art').AsString := FArticuloId;
      dmgMain.qryExec.ExecSQL;
      
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_articulos_ingredientes WHERE id_articulo = :id_art';
      dmgMain.qryExec.ParamByName('id_art').AsString := FArticuloId;
      dmgMain.qryExec.ExecSQL;
    except
      // Ignorar fallos de limpieza
    end;
  end;
end;
procedure TfrmArticulosEditor.btnAddAlergenoClick(Sender: TObject);
var
  i: Integer;
  LAllergenId: string;
begin
  if not Assigned(frmSelectAlergeno) then
    Application.CreateForm(TfrmSelectAlergeno, frmSelectAlergeno);
    
  frmSelectAlergeno.MultiSelect := True;
  
  if frmSelectAlergeno.ShowModal = mrOk then
  begin
    for i := 0 to frmSelectAlergeno.SelectedIds.Count - 1 do
    begin
      LAllergenId := frmSelectAlergeno.SelectedIds[i];
      
      // Verificar si ya existe para evitar duplicados
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 
        'SELECT COUNT(*) FROM ge_articulos_alergenos ' +
        'WHERE id_articulo = :id_art AND id_alergeno = :id_alg';
      dmgMain.qryExec.ParamByName('id_art').AsString := FArticuloId;
      dmgMain.qryExec.ParamByName('id_alg').AsString := LAllergenId;
      dmgMain.qryExec.Open;
      
      if dmgMain.qryExec.Fields[0].AsInteger = 0 then
      begin
        // Insertar asociación
        dmgMain.qryExec.Close;
        dmgMain.qryExec.SQL.Text :=
          'INSERT INTO ge_articulos_alergenos (id_articulo, id_alergeno, presencia, calculado_receta, id_user_creator, id_user_update) ' +
          'VALUES (:id_art, :id_alg, '''', 0, :user, :user)';
        dmgMain.qryExec.ParamByName('id_art').AsString := FArticuloId;
        dmgMain.qryExec.ParamByName('id_alg').AsString := LAllergenId;
        dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
        try
          dmgMain.qryExec.ExecSQL;
        except
          on E: Exception do
            TDbErrorHandler.HandleException(E, dmgMain.qryExec.SQL.Text);
        end;
      end;
    end;
    
    // Recargar alérgenos
    qryAlergenos.Close;
    qryAlergenos.ParamByName('id_articulo').AsString := FArticuloId;
    qryAlergenos.Open;
  end;
end;
procedure TfrmArticulosEditor.btnDelAlergenoClick(Sender: TObject);
var
  LAssocId: string;
begin
  if not qryAlergenos.IsEmpty then
  begin
    LAssocId := qryAlergenos.FieldByName('id').AsString;
    
    if MessageDlg('¿Está seguro de que desea eliminar el alérgeno seleccionado de este artículo?', 
                  mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    begin
      dmgMain.qryExec.Close;
      dmgMain.qryExec.SQL.Text := 'DELETE FROM ge_articulos_alergenos WHERE id = :id';
      dmgMain.qryExec.ParamByName('id').AsString := LAssocId;
      try
        dmgMain.qryExec.ExecSQL;
        
        // Recargar alérgenos
        qryAlergenos.Close;
        qryAlergenos.ParamByName('id_articulo').AsString := FArticuloId;
        qryAlergenos.Open;
      except
        on E: Exception do
          TDbErrorHandler.HandleException(E, dmgMain.qryExec.SQL.Text);
      end;
    end;
  end;
end;
procedure TfrmArticulosEditor.qryAlergenosUpdateRecord(ASender: TDataSet;
  ARequest: TFDUpdateRequest; var AAction: TFDErrorAction; AOptions: TFDUpdateRowOptions);
begin
  if ARequest = arUpdate then
  begin
    dmgMain.qryExec.Close;
    dmgMain.qryExec.SQL.Text := 
      'UPDATE ge_articulos_alergenos SET presencia = :pres, id_user_update = :user, updated_at = CURRENT_TIMESTAMP ' +
      'WHERE id = :id';
    dmgMain.qryExec.ParamByName('id').AsString := ASender.FieldByName('id').AsString;
    dmgMain.qryExec.ParamByName('pres').AsString := ASender.FieldByName('presencia').AsString;
    dmgMain.qryExec.ParamByName('user').AsString := dmgMain.CurrentUserId;
    dmgMain.qryExec.ExecSQL;
    AAction := eaApplied;
  end;
end;
procedure TfrmArticulosEditor.LoadProductImage;
var
  Qry: TFDQuery;
  Stream: TMemoryStream;
begin
  imgProducto.Picture := nil;
  if FArticuloId = '' then Exit;
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT archivo FROM ge_documentos WHERE id_articulo = :id_art AND descripcion = ''IMAGEN_PRODUCTO'' LIMIT 1';
    Qry.ParamByName('id_art').AsString := FArticuloId;
    Qry.Open;
    if not Qry.IsEmpty and not TBlobField(Qry.FieldByName('archivo')).IsNull then
    begin
      Stream := TMemoryStream.Create;
      try
        TBlobField(Qry.FieldByName('archivo')).SaveToStream(Stream);
        Stream.Position := 0;
        if Stream.Size > 0 then
          imgProducto.Picture.LoadFromStream(Stream);
      finally
        Stream.Free;
      end;
    end;
  finally
    Qry.Free;
  end;
end;
procedure TfrmArticulosEditor.LoadDocuments;
begin
  if FArticuloId <> '' then
  begin
    qryDocumentos.Close;
    qryDocumentos.ParamByName('id_articulo').AsString := FArticuloId;
    qryDocumentos.Open;
  end
  else
    qryDocumentos.Close;
end;
procedure TfrmArticulosEditor.btnSubirImagenClick(Sender: TObject);
var
  OpenDlg: TOpenDialog;
  Qry: TFDQuery;
  Stream: TMemoryStream;
  LFileName, LExtension: string;
begin
  if FIsNewRecord then
  begin
    ShowMessage('Guarde primero el artículo antes de asignarle una imagen.');
    Exit;
  end;
  OpenDlg := TOpenDialog.Create(nil);
  try
    OpenDlg.Filter := 'Imágenes (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg';
    if OpenDlg.Execute then
    begin
      LFileName := ExtractFileName(OpenDlg.FileName);
      LExtension := LowerCase(ExtractFileExt(OpenDlg.FileName));
      if LExtension <> '' then
        LExtension := Copy(LExtension, 2, Length(LExtension) - 1);
      
      Stream := TMemoryStream.Create;
      try
        Stream.LoadFromFile(OpenDlg.FileName);
        Stream.Position := 0;
        
        Qry := TFDQuery.Create(nil);
        try
          Qry.Connection := dmgMain.dbConn;
          Qry.SQL.Text := 'SELECT id FROM ge_documentos WHERE id_articulo = :id_art AND descripcion = ''IMAGEN_PRODUCTO'' LIMIT 1';
          Qry.ParamByName('id_art').AsString := FArticuloId;
          Qry.Open;
          
          if Qry.IsEmpty then
          begin
            Qry.Close;
            Qry.SQL.Text := 
              'INSERT INTO ge_documentos (id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, id_articulo, id_user_creator, id_user_update) ' +
              'VALUES (:id_empresa, :fecha, ''IMAGEN_PRODUCTO'', :archivo, :nombre_archivo, :extension, :id_articulo, :user, :user)';
          end
          else
          begin
            LFileName := Qry.FieldByName('id').AsString;
            Qry.Close;
            Qry.SQL.Text := 
              'UPDATE ge_documentos SET fecha = :fecha, archivo = :archivo, nombre_archivo = :nombre_archivo, ' +
              '  extension = :extension, id_user_update = :user, update_at = CURRENT_TIMESTAMP ' +
              'WHERE id = :id';
            Qry.ParamByName('id').AsString := LFileName;
          end;
          
          Qry.ParamByName('fecha').AsDate := Date;
          Qry.ParamByName('archivo').LoadFromStream(Stream, ftBlob);
          Qry.ParamByName('nombre_archivo').DataType := ftWideString;
          Qry.ParamByName('nombre_archivo').AsWideString := ExtractFileName(OpenDlg.FileName);
          Qry.ParamByName('extension').DataType := ftWideString;
          Qry.ParamByName('extension').AsWideString := LExtension;
          Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
          if Qry.SQL.Text.StartsWith('INSERT') then
          begin
            Qry.ParamByName('id_empresa').AsString := dmgMain.CurrentCompanyId;
            Qry.ParamByName('id_articulo').AsString := FArticuloId;
          end;
          
          Qry.ExecSQL;
        finally
          Qry.Free;
        end;
        
        LoadProductImage;
      finally
        Stream.Free;
      end;
    end;
  finally
    OpenDlg.Free;
  end;
end;
procedure TfrmArticulosEditor.btnEliminarImagenClick(Sender: TObject);
var
  Qry: TFDQuery;
begin
  if FArticuloId = '' then Exit;
  if MessageDlg('¿Está seguro de que desea eliminar la imagen de este artículo?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'DELETE FROM ge_documentos WHERE id_articulo = :id_art AND descripcion = ''IMAGEN_PRODUCTO''';
      Qry.ParamByName('id_art').AsString := FArticuloId;
      Qry.ExecSQL;
    finally
      Qry.Free;
    end;
    imgProducto.Picture := nil;
  end;
end;
procedure TfrmArticulosEditor.btnSubirDocClick(Sender: TObject);
var
  LDesc: string;
  LFileName, LExtension: string;
  Stream: TMemoryStream;
  Qry: TFDQuery;
begin
  if FIsNewRecord then
  begin
    ShowMessage('Guarde primero el artículo antes de asignarle documentos.');
    Exit;
  end;
  LDesc := InputBox('Subir Documento', 'Introduzca una descripción para el documento:', '');
  if Trim(LDesc) = '' then
  begin
    ShowMessage('La descripción es obligatoria.');
    Exit;
  end;
  if odDocumento.Execute then
  begin
    LFileName := ExtractFileName(odDocumento.FileName);
    LExtension := LowerCase(ExtractFileExt(odDocumento.FileName));
    if LExtension <> '' then
      LExtension := Copy(LExtension, 2, Length(LExtension) - 1);
      
    Stream := TMemoryStream.Create;
    try
      Stream.LoadFromFile(odDocumento.FileName);
      Stream.Position := 0;
      
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 
          'INSERT INTO ge_documentos (id_empresa, fecha, descripcion, archivo, nombre_archivo, extension, id_articulo, id_user_creator, id_user_update) ' +
          'VALUES (:id_empresa, :fecha, :descripcion, :archivo, :nombre_archivo, :extension, :id_articulo, :user, :user)';
        Qry.ParamByName('id_empresa').AsString := dmgMain.CurrentCompanyId;
        Qry.ParamByName('fecha').AsDate := Date;
        Qry.ParamByName('descripcion').DataType := ftWideString;
        Qry.ParamByName('descripcion').AsWideString := LDesc;
        Qry.ParamByName('archivo').LoadFromStream(Stream, ftBlob);
        Qry.ParamByName('nombre_archivo').DataType := ftWideString;
        Qry.ParamByName('nombre_archivo').AsWideString := LFileName;
        Qry.ParamByName('extension').DataType := ftWideString;
        Qry.ParamByName('extension').AsWideString := LExtension;
        Qry.ParamByName('id_articulo').AsString := FArticuloId;
        Qry.ParamByName('user').AsString := dmgMain.CurrentUserId;
        Qry.ExecSQL;
      finally
        Qry.Free;
      end;
      LoadDocuments;
    finally
      Stream.Free;
    end;
  end;
end;
procedure TfrmArticulosEditor.btnVerDocClick(Sender: TObject);
var
  Qry: TFDQuery;
  BlobField: TBlobField;
  TempPath, TempFile: string;
  Stream: TMemoryStream;
  LDocId: string;
  DocNombre: string;
begin
  if qryDocumentos.IsEmpty then Exit;
  LDocId := qryDocumentos.FieldByName('id').AsString;
  if LDocId = '' then Exit;
  
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := dmgMain.dbConn;
    Qry.SQL.Text := 'SELECT archivo, nombre_archivo, extension FROM ge_documentos WHERE id = :id';
    Qry.ParamByName('id').AsString := LDocId;
    Qry.Open;
    if not Qry.IsEmpty then
    begin
      BlobField := TBlobField(Qry.FieldByName('archivo'));
      if BlobField.IsNull or (BlobField.BlobSize = 0) then
      begin
        ShowMessage('El documento está vacío.');
        Exit;
      end;
      DocNombre := Qry.FieldByName('nombre_archivo').AsString;
      TempPath := TPath.GetTempPath;
      TempFile := TPath.Combine(TempPath, DocNombre);
      if TempFile = '' then
        TempFile := TPath.Combine(TempPath, 'doc_' + LDocId + '.' + Qry.FieldByName('extension').AsString);
      Stream := TMemoryStream.Create;
      try
        BlobField.SaveToStream(Stream);
        Stream.Position := 0;
        Stream.SaveToFile(TempFile);
      finally
        Stream.Free;
      end;
      ShellExecute(0, 'open', PChar(TempFile), nil, nil, SW_SHOWNORMAL);
    end;
  finally
    Qry.Free;
  end;
end;
procedure TfrmArticulosEditor.btnEliminarDocClick(Sender: TObject);
var
  LDocId: string;
  Qry: TFDQuery;
begin
  if qryDocumentos.IsEmpty then Exit;
  LDocId := qryDocumentos.FieldByName('id').AsString;
  if LDocId = '' then Exit;
  if MessageDlg('¿Está seguro de que desea eliminar el documento seleccionado?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 'DELETE FROM ge_documentos WHERE id = :id';
      Qry.ParamByName('id').AsString := LDocId;
      Qry.ExecSQL;
    finally
      Qry.Free;
    end;
    LoadDocuments;
  end;
end;

function TfrmArticulosEditor.GetSelectedIvaRate: Double;
begin
  Result := 0;
  if Assigned(FIvaPorcentajes) and (cbTipoIva.ItemIndex >= 0) and (cbTipoIva.ItemIndex < FIvaPorcentajes.Count) then
  begin
    var LStrRate := FIvaPorcentajes[cbTipoIva.ItemIndex];
    LStrRate := StringReplace(LStrRate, '.', FormatSettings.DecimalSeparator, []);
    LStrRate := StringReplace(LStrRate, ',', FormatSettings.DecimalSeparator, []);
    Result := StrToFloatDef(LStrRate, 0);
  end;
end;

procedure TfrmArticulosEditor.RecalcularPrecioConIva(AEdtSinIva, AEdtConIva: TEdit);
var
  LSinIva, LConIva, LIvaRate: Double;
begin
  if FUpdatingPrices then Exit;
  FUpdatingPrices := True;
  try
    LIvaRate := GetSelectedIvaRate;
    LSinIva := ParseFloatSeguro(AEdtSinIva.Text);
    LConIva := LSinIva * (1 + (LIvaRate / 100));
    AEdtConIva.Text := FormatFloat('#,##0.00', LConIva);
  finally
    FUpdatingPrices := False;
  end;
end;

procedure TfrmArticulosEditor.RecalcularPrecioSinIva(AEdtConIva, AEdtSinIva: TEdit);
var
  LSinIva, LConIva, LIvaRate: Double;
begin
  if FUpdatingPrices then Exit;
  FUpdatingPrices := True;
  try
    LIvaRate := GetSelectedIvaRate;
    LConIva := ParseFloatSeguro(AEdtConIva.Text);
    if LIvaRate <> -100 then
      LSinIva := LConIva / (1 + (LIvaRate / 100))
    else
      LSinIva := 0;
    AEdtSinIva.Text := FormatFloat('#,##0.00', LSinIva);
  finally
    FUpdatingPrices := False;
  end;
end;

procedure TfrmArticulosEditor.RecalcularTodosConIva;
begin
  RecalcularPrecioConIva(edtPvp1, edtPvp1Iva);
  RecalcularPrecioConIva(edtPvp2, edtPvp2Iva);
  RecalcularPrecioConIva(edtPvp3, edtPvp3Iva);
  RecalcularPrecioConIva(edtPvp4, edtPvp4Iva);
end;

procedure TfrmArticulosEditor.edtPvpSinIvaChange(Sender: TObject);
begin
  if not (Sender is TEdit) then Exit;
  var LEdtSinIva := TEdit(Sender);
  var LEdtConIva: TEdit := nil;
  
  if LEdtSinIva = edtPvp1 then LEdtConIva := edtPvp1Iva
  else if LEdtSinIva = edtPvp2 then LEdtConIva := edtPvp2Iva
  else if LEdtSinIva = edtPvp3 then LEdtConIva := edtPvp3Iva
  else if LEdtSinIva = edtPvp4 then LEdtConIva := edtPvp4Iva;
  
  if Assigned(LEdtConIva) then
    RecalcularPrecioConIva(LEdtSinIva, LEdtConIva);
end;

procedure TfrmArticulosEditor.edtPvpConIvaChange(Sender: TObject);
begin
  if not (Sender is TEdit) then Exit;
  var LEdtConIva := TEdit(Sender);
  var LEdtSinIva: TEdit := nil;
  
  if LEdtConIva = edtPvp1Iva then LEdtSinIva := edtPvp1
  else if LEdtConIva = edtPvp2Iva then LEdtSinIva := edtPvp2
  else if LEdtConIva = edtPvp3Iva then LEdtSinIva := edtPvp3
  else if LEdtConIva = edtPvp4Iva then LEdtSinIva := edtPvp4;
  
  if Assigned(LEdtSinIva) then
    RecalcularPrecioSinIva(LEdtConIva, LEdtSinIva);
end;

procedure TfrmArticulosEditor.cbTipoIvaChange(Sender: TObject);
begin
  RecalcularTodosConIva;
end;

procedure TfrmArticulosEditor.btnBoldClick(Sender: TObject);
begin
  if fsBold in redtDescripcionEtiqueta.SelAttributes.Style then
    redtDescripcionEtiqueta.SelAttributes.Style := redtDescripcionEtiqueta.SelAttributes.Style - [fsBold]
  else
    redtDescripcionEtiqueta.SelAttributes.Style := redtDescripcionEtiqueta.SelAttributes.Style + [fsBold];
  UpdateFormatToolbarState;
  redtDescripcionEtiqueta.SetFocus;
end;

procedure TfrmArticulosEditor.btnUnderlineClick(Sender: TObject);
begin
  if fsUnderline in redtDescripcionEtiqueta.SelAttributes.Style then
    redtDescripcionEtiqueta.SelAttributes.Style := redtDescripcionEtiqueta.SelAttributes.Style - [fsUnderline]
  else
    redtDescripcionEtiqueta.SelAttributes.Style := redtDescripcionEtiqueta.SelAttributes.Style + [fsUnderline];
  UpdateFormatToolbarState;
  redtDescripcionEtiqueta.SetFocus;
end;

procedure TfrmArticulosEditor.cbFontSizeChange(Sender: TObject);
var
  LSize: Integer;
begin
  if cbFontSize.ItemIndex >= 0 then
  begin
    LSize := StrToIntDef(cbFontSize.Items[cbFontSize.ItemIndex], 0);
    if LSize > 0 then
      redtDescripcionEtiqueta.SelAttributes.Size := LSize;
  end;
  redtDescripcionEtiqueta.SetFocus;
end;

procedure TfrmArticulosEditor.redtDescripcionEtiquetaSelectionChange(Sender: TObject);
begin
  UpdateFormatToolbarState;
end;

procedure TfrmArticulosEditor.btnBoldIngredientesClick(Sender: TObject);
begin
  ToggleRichEditBold(redtIngredientes, btnBoldIngredientes);
end;

procedure TfrmArticulosEditor.btnUnderlineIngredientesClick(Sender: TObject);
begin
  ToggleRichEditUnderline(redtIngredientes, btnUnderlineIngredientes);
end;

procedure TfrmArticulosEditor.redtIngredientesSelectionChange(Sender: TObject);
begin
  UpdateRichEditToolbar(redtIngredientes, btnBoldIngredientes, btnUnderlineIngredientes);
end;

procedure TfrmArticulosEditor.UpdateFormatToolbarState;
var
  LSize: Integer;
  LIdx: Integer;
begin
  btnBold.Down := fsBold in redtDescripcionEtiqueta.SelAttributes.Style;
  btnUnderline.Down := fsUnderline in redtDescripcionEtiqueta.SelAttributes.Style;
  LSize := redtDescripcionEtiqueta.SelAttributes.Size;
  if LSize > 0 then
  begin
    LIdx := cbFontSize.Items.IndexOf(IntToStr(LSize));
    if LIdx >= 0 then
      cbFontSize.ItemIndex := LIdx;
  end;
end;

procedure TfrmArticulosEditor.btnSourceDescClick(Sender: TObject);
begin
  ToggleRichEditSourceView(redtDescripcionEtiqueta, btnSourceDesc);
  btnBold.Enabled := not btnSourceDesc.Down;
  btnUnderline.Enabled := not btnSourceDesc.Down;
  cbFontSize.Enabled := not btnSourceDesc.Down;
end;

procedure TfrmArticulosEditor.btnSourceIngredientesClick(Sender: TObject);
begin
  ToggleRichEditSourceView(redtIngredientes, btnSourceIngredientes);
  btnBoldIngredientes.Enabled := not btnSourceIngredientes.Down;
  btnUnderlineIngredientes.Enabled := not btnSourceIngredientes.Down;
end;

end.
